feat: add react-i18next with English and Hindi locale support.
- Add i18next + react-i18next + i18next-browser-languagedetector. - EN/HI translation files covering all UI strings across every page and component. - Language switcher button in Navbar; choice persisted to localStorage. - document.documentElement.lang synced to active language in App. - Skip-nav link and #main-content anchor for keyboard accessibility. - aria-describedby on modal dialog; page title and meta description in index.html. - Secure page title set to 'BIS SP-21 Standards.'
This commit is contained in:
@@ -4,9 +4,11 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>client</title>
|
<meta name="description" content="BIS SP-21 Standards — search and explore Bureau of Indian Standards building material standards." />
|
||||||
|
<title>BIS SP‑21 Standards</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<a href="#main-content" class="skip-nav">Skip to main content</a>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Generated
+103
-30
@@ -8,8 +8,11 @@
|
|||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"i18next": "^26.0.8",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-i18next": "^17.0.6",
|
||||||
"react-router-dom": "^7.14.2"
|
"react-router-dom": "^7.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -216,6 +219,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||||
|
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -664,9 +676,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -684,9 +693,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -704,9 +710,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -724,9 +727,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -744,9 +744,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -764,9 +761,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1509,6 +1503,52 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-parse-stringify": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"void-elements": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "26.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.8.tgz",
|
||||||
|
"integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.locize.com/i18next"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.locize.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5 || ^6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next-browser-languagedetector": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.23.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -1780,9 +1820,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1804,9 +1841,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1828,9 +1862,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1852,9 +1883,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2152,6 +2180,33 @@
|
|||||||
"react": "^19.2.5"
|
"react": "^19.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-i18next": {
|
||||||
|
"version": "17.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.6.tgz",
|
||||||
|
"integrity": "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.2",
|
||||||
|
"html-parse-stringify": "^3.0.1",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"i18next": ">= 26.0.1",
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"typescript": "^5 || ^6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.14.2",
|
"version": "7.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
||||||
@@ -2365,6 +2420,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.10",
|
"version": "8.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||||
@@ -2443,6 +2507,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"i18next": "^26.0.8",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-i18next": "^17.0.6",
|
||||||
"react-router-dom": "^7.14.2"
|
"react-router-dom": "^7.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+10
-4
@@ -1,4 +1,6 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { Routes, Route, Navigate } from "react-router-dom";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import Navbar from "./components/Navbar";
|
import Navbar from "./components/Navbar";
|
||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
@@ -7,14 +9,17 @@ import Categories from "./pages/Categories";
|
|||||||
import About from "./pages/About";
|
import About from "./pages/About";
|
||||||
import Recommend from "./pages/Recommend";
|
import Recommend from "./pages/Recommend";
|
||||||
|
|
||||||
/**
|
|
||||||
* Main application router.
|
|
||||||
* Renders layout with Navbar/Footer and routes to pages.
|
|
||||||
*/
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = i18n.language;
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
<div id="main-content">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/standards" element={<Standards />} />
|
<Route path="/standards" element={<Standards />} />
|
||||||
@@ -23,6 +28,7 @@ export default function App() {
|
|||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import "./Footer.css";
|
import "./Footer.css";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer" role="contentinfo">
|
<footer className="footer" role="contentinfo">
|
||||||
<div className="footer-inner">
|
<div className="footer-inner">
|
||||||
<div className="footer-cols">
|
<div className="footer-cols">
|
||||||
<div className="footer-col">
|
<div className="footer-col">
|
||||||
<p className="footer-brand">BIS SP‑21</p>
|
<p className="footer-brand">{t("footer.brand")}</p>
|
||||||
<p className="footer-tagline">
|
<p className="footer-tagline">{t("footer.tagline")}</p>
|
||||||
Handbook on Building Materials<br />
|
|
||||||
Special Publication 21 : 2005
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-col">
|
<div className="footer-col">
|
||||||
<p className="footer-heading">Portal</p>
|
<p className="footer-heading">{t("footer.portal")}</p>
|
||||||
<Link className="footer-link" to="/standards">Search Standards</Link>
|
<Link className="footer-link" to="/standards">{t("footer.searchStandards")}</Link>
|
||||||
<Link className="footer-link" to="/categories">Browse Categories</Link>
|
<Link className="footer-link" to="/categories">{t("footer.browseCategories")}</Link>
|
||||||
<Link className="footer-link" to="/about">About</Link>
|
<Link className="footer-link" to="/about">{t("footer.about")}</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-col">
|
<div className="footer-col">
|
||||||
<p className="footer-heading">Bureau of Indian Standards</p>
|
<p className="footer-heading">{t("footer.bis")}</p>
|
||||||
<a className="footer-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">BIS Official Website</a>
|
<a className="footer-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">{t("footer.bisWebsite")}</a>
|
||||||
<a className="footer-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">Manak Online</a>
|
<a className="footer-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">{t("footer.manakOnline")}</a>
|
||||||
<a className="footer-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">Standards Portal</a>
|
<a className="footer-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">{t("footer.standardsPortal")}</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-col">
|
<div className="footer-col">
|
||||||
<p className="footer-heading">Ministry</p>
|
<p className="footer-heading">{t("footer.ministry")}</p>
|
||||||
<a className="footer-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">DPIIT</a>
|
<a className="footer-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">{t("footer.dpiit")}</a>
|
||||||
<a className="footer-link" href="https://www.india.gov.in" target="_blank" rel="noopener noreferrer">National Portal</a>
|
<a className="footer-link" href="https://www.india.gov.in" target="_blank" rel="noopener noreferrer">{t("footer.nationalPortal")}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-legal">
|
<div className="footer-legal">
|
||||||
<p>© Bureau of Indian Standards, Ministry of Commerce & Industry, Government of India. All rights reserved.</p>
|
<p>{t("footer.copyright")}</p>
|
||||||
<p>Content sourced from BIS Special Publication 21 : 2005. For official standards, refer to{" "}
|
<p>{t("footer.sourceNote")}{" "}
|
||||||
<a href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer" className="legal-link">bis.gov.in</a>.
|
<a href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer" className="legal-link">bis.gov.in</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,6 +90,20 @@
|
|||||||
}
|
}
|
||||||
.mobile-link:hover { background: rgba(255,255,255,0.05); }
|
.mobile-link:hover { background: rgba(255,255,255,0.05); }
|
||||||
|
|
||||||
|
.nav-lang-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid rgba(255,255,255,0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.nav-lang-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border-color: rgba(255,255,255,0.45);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 833px) {
|
@media (max-width: 833px) {
|
||||||
.nav-links { display: none; }
|
.nav-links { display: none; }
|
||||||
.nav-hamburger { display: flex; }
|
.nav-hamburger { display: flex; }
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import "./Navbar.css";
|
import "./Navbar.css";
|
||||||
|
|
||||||
const NAV_LINKS = [
|
|
||||||
{ label: "Standards", to: "/standards" },
|
|
||||||
{ label: "Categories", to: "/categories" },
|
|
||||||
{ label: "✦ AI Recommend", to: "/recommend" },
|
|
||||||
{ label: "About", to: "/about" },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Site navigation bar with responsive menu.
|
|
||||||
*/
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const NAV_LINKS = [
|
||||||
|
{ label: t("nav.standards"), to: "/standards" },
|
||||||
|
{ label: t("nav.categories"), to: "/categories" },
|
||||||
|
{ label: t("nav.recommend"), to: "/recommend" },
|
||||||
|
{ label: t("nav.about"), to: "/about" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleLang = () => {
|
||||||
|
i18n.changeLanguage(i18n.language === "en" ? "hi" : "en");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="global-nav" role="navigation" aria-label="Primary navigation">
|
<nav className="global-nav" role="navigation" aria-label={t("nav.brand") + " primary navigation"}>
|
||||||
<div className="nav-inner">
|
<div className="nav-inner">
|
||||||
<Link className="nav-emblem" to="/" aria-label="BIS SP-21 home" onClick={() => setOpen(false)}>
|
<Link className="nav-emblem" to="/" aria-label={t("nav.brand") + " home"} onClick={() => setOpen(false)}>
|
||||||
<BISIcon />
|
<BISIcon />
|
||||||
<span className="nav-brand">BIS SP‑21</span>
|
<span className="nav-brand">{t("nav.brand")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="nav-links" role="list">
|
<div className="nav-links" role="list">
|
||||||
@@ -43,13 +46,21 @@ export default function Navbar() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
>
|
>
|
||||||
BIS Portal ↗
|
{t("nav.bisPortal")}
|
||||||
</a>
|
</a>
|
||||||
|
<button
|
||||||
|
className="nav-link nav-lang-btn"
|
||||||
|
onClick={toggleLang}
|
||||||
|
aria-label={t("lang.switchTo")}
|
||||||
|
title={t("lang.switchTo")}
|
||||||
|
>
|
||||||
|
{i18n.language === "en" ? t("lang.hi") : t("lang.en")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="nav-hamburger"
|
className="nav-hamburger"
|
||||||
aria-label={open ? "Close menu" : "Open menu"}
|
aria-label={open ? t("nav.closeMenu") : t("nav.openMenu")}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls="mobile-menu"
|
aria-controls="mobile-menu"
|
||||||
onClick={() => setOpen((o) => !o)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
@@ -80,8 +91,15 @@ export default function Navbar() {
|
|||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
BIS Portal ↗
|
{t("nav.bisPortal")}
|
||||||
</a>
|
</a>
|
||||||
|
<button
|
||||||
|
className="mobile-link nav-lang-btn"
|
||||||
|
onClick={() => { toggleLang(); setOpen(false); }}
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
{i18n.language === "en" ? t("lang.hi") : t("lang.en")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import "./StandardCard.css";
|
import "./StandardCard.css";
|
||||||
|
|
||||||
export default function StandardCard({ standard, onClick }) {
|
export default function StandardCard({ standard, onClick }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const sectionCount = Object.keys(standard.key_sections || {}).length;
|
const sectionCount = Object.keys(standard.key_sections || {}).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -10,7 +12,7 @@ export default function StandardCard({ standard, onClick }) {
|
|||||||
onKeyDown={(e) => e.key === "Enter" && onClick()}
|
onKeyDown={(e) => e.key === "Enter" && onClick()}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`View details for ${standard.standard_id}`}
|
aria-label={t("card.viewDetails", { id: standard.standard_id })}
|
||||||
>
|
>
|
||||||
<span className="card-cat">{standard.category}</span>
|
<span className="card-cat">{standard.category}</span>
|
||||||
<p className="card-id">{standard.standard_id}</p>
|
<p className="card-id">{standard.standard_id}</p>
|
||||||
@@ -19,7 +21,7 @@ export default function StandardCard({ standard, onClick }) {
|
|||||||
<p className="card-summary">{standard.summary}</p>
|
<p className="card-summary">{standard.summary}</p>
|
||||||
)}
|
)}
|
||||||
{standard.keywords?.length > 0 && (
|
{standard.keywords?.length > 0 && (
|
||||||
<div className="card-keywords" aria-label="Keywords">
|
<div className="card-keywords" aria-label={t("card.keywords")}>
|
||||||
{standard.keywords.slice(0, 5).map((kw) => (
|
{standard.keywords.slice(0, 5).map((kw) => (
|
||||||
<span className="keyword-chip" key={kw}>{kw}</span>
|
<span className="keyword-chip" key={kw}>{kw}</span>
|
||||||
))}
|
))}
|
||||||
@@ -27,7 +29,7 @@ export default function StandardCard({ standard, onClick }) {
|
|||||||
)}
|
)}
|
||||||
<div className="card-footer">
|
<div className="card-footer">
|
||||||
<span className="card-sections-count">
|
<span className="card-sections-count">
|
||||||
{sectionCount} section{sectionCount !== 1 ? "s" : ""}
|
{t("card.section", { count: sectionCount })}
|
||||||
</span>
|
</span>
|
||||||
<span className="card-arrow" aria-hidden="true">→</span>
|
<span className="card-arrow" aria-hidden="true">→</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { askQuestion } from "../api/standards";
|
import { askQuestion } from "../api/standards";
|
||||||
import "./StandardModal.css";
|
import "./StandardModal.css";
|
||||||
|
|
||||||
export default function StandardModal({ standard, onClose }) {
|
export default function StandardModal({ standard, onClose }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const modalRef = useRef(null);
|
const modalRef = useRef(null);
|
||||||
const closeBtnRef = useRef(null);
|
const closeBtnRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const [question, setQuestion] = useState("");
|
const [question, setQuestion] = useState("");
|
||||||
const [messages, setMessages] = useState([]); // [{role, text}]
|
const [messages, setMessages] = useState([]);
|
||||||
const [asking, setAsking] = useState(false);
|
const [asking, setAsking] = useState(false);
|
||||||
const [aiError, setAiError] = useState(null);
|
const [aiError, setAiError] = useState(null);
|
||||||
const chatEndRef = useRef(null);
|
const chatEndRef = useRef(null);
|
||||||
@@ -46,7 +48,7 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
const { answer } = await askQuestion({ standard_id: standard.standard_id, question: q });
|
const { answer } = await askQuestion({ standard_id: standard.standard_id, question: q });
|
||||||
setMessages((prev) => [...prev, { role: "ai", text: answer }]);
|
setMessages((prev) => [...prev, { role: "ai", text: answer }]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAiError(err.message || "Something went wrong. Please try again.");
|
setAiError(err.message || t("common.serverError"));
|
||||||
} finally {
|
} finally {
|
||||||
setAsking(false);
|
setAsking(false);
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
@@ -69,6 +71,7 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-title"
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-summary"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -82,7 +85,7 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
className="modal-close"
|
className="modal-close"
|
||||||
ref={closeBtnRef}
|
ref={closeBtnRef}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close standard detail"
|
aria-label={t("modal.closeLabel")}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@@ -91,15 +94,15 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
{/* Standard detail */}
|
{/* Standard detail */}
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
{standard.summary && (
|
{standard.summary && (
|
||||||
<div className="modal-section">
|
<div className="modal-section" id="modal-summary">
|
||||||
<p className="modal-section-title">Summary</p>
|
<p className="modal-section-title">{t("modal.summary")}</p>
|
||||||
<p className="modal-section-body">{standard.summary}</p>
|
<p className="modal-section-body">{standard.summary}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{standard.keywords?.length > 0 && (
|
{standard.keywords?.length > 0 && (
|
||||||
<div className="modal-section">
|
<div className="modal-section">
|
||||||
<p className="modal-section-title">Keywords</p>
|
<p className="modal-section-title">{t("modal.keywords")}</p>
|
||||||
<div className="modal-keywords">
|
<div className="modal-keywords">
|
||||||
{standard.keywords.map((kw) => (
|
{standard.keywords.map((kw) => (
|
||||||
<span className="modal-keyword" key={kw}>{kw}</span>
|
<span className="modal-keyword" key={kw}>{kw}</span>
|
||||||
@@ -110,7 +113,7 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
|
|
||||||
{sections.length > 0 && (
|
{sections.length > 0 && (
|
||||||
<div className="modal-section">
|
<div className="modal-section">
|
||||||
<p className="modal-section-title">Key Sections</p>
|
<p className="modal-section-title">{t("modal.keySections")}</p>
|
||||||
<div className="modal-key-sections">
|
<div className="modal-key-sections">
|
||||||
{sections.map(([name, text]) => (
|
{sections.map(([name, text]) => (
|
||||||
<div className="key-section-item" key={name}>
|
<div className="key-section-item" key={name}>
|
||||||
@@ -123,24 +126,24 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI chat panel */}
|
{/* AI chat panel */}
|
||||||
<div className="modal-section ai-panel" aria-label="Ask AI about this standard">
|
<div className="modal-section ai-panel" aria-label={t("modal.askAI")}>
|
||||||
<p className="modal-section-title">
|
<p className="modal-section-title">
|
||||||
<span className="ai-label-icon" aria-hidden="true">✦</span>
|
<span className="ai-label-icon" aria-hidden="true">✦</span>
|
||||||
Ask AI about this standard
|
{t("modal.askAI")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<div className="chat-log" aria-live="polite" aria-label="Conversation">
|
<div className="chat-log" aria-live="polite" aria-label={t("modal.conversation")}>
|
||||||
{messages.map((m, i) => (
|
{messages.map((m, i) => (
|
||||||
<div key={i} className={`chat-bubble chat-bubble--${m.role}`}>
|
<div key={i} className={`chat-bubble chat-bubble--${m.role}`}>
|
||||||
{m.role === "ai" && (
|
{m.role === "ai" && (
|
||||||
<span className="bubble-label" aria-label="AI response">✦</span>
|
<span className="bubble-label" aria-label={t("modal.aiResponse")}>✦</span>
|
||||||
)}
|
)}
|
||||||
<p className="bubble-text">{m.text}</p>
|
<p className="bubble-text">{m.text}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{asking && (
|
{asking && (
|
||||||
<div className="chat-bubble chat-bubble--ai chat-bubble--loading" aria-label="AI is thinking">
|
<div className="chat-bubble chat-bubble--ai chat-bubble--loading" aria-label={t("modal.aiThinking")}>
|
||||||
<span className="bubble-label" aria-hidden="true">✦</span>
|
<span className="bubble-label" aria-hidden="true">✦</span>
|
||||||
<span className="typing-dots">
|
<span className="typing-dots">
|
||||||
<span /><span /><span />
|
<span /><span /><span />
|
||||||
@@ -155,8 +158,8 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{messages.length === 0 && !asking && (
|
{messages.length === 0 && !asking && (
|
||||||
<div className="chat-suggestions" aria-label="Suggested questions">
|
<div className="chat-suggestions" aria-label={t("modal.suggestionsLabel")}>
|
||||||
{getSuggestions(standard).map((s) => (
|
{getSuggestions(standard, t).map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
className="suggestion-chip"
|
className="suggestion-chip"
|
||||||
@@ -171,25 +174,25 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form className="chat-form" onSubmit={handleAsk} aria-label="Ask a question">
|
<form className="chat-form" onSubmit={handleAsk} aria-label={t("modal.askAI")}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="chat-input"
|
className="chat-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={question}
|
value={question}
|
||||||
onChange={(e) => setQuestion(e.target.value)}
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
placeholder="Ask a question about this standard…"
|
placeholder={t("modal.chatPlaceholder")}
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
disabled={asking}
|
disabled={asking}
|
||||||
aria-label="Your question"
|
aria-label={t("modal.questionLabel")}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="chat-send"
|
className="chat-send"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!question.trim() || asking}
|
disabled={!question.trim() || asking}
|
||||||
aria-label="Send question"
|
aria-label={t("modal.sendLabel")}
|
||||||
>
|
>
|
||||||
{asking ? "…" : "Ask"}
|
{asking ? t("modal.sending") : t("modal.askBtn")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,17 +202,17 @@ export default function StandardModal({ standard, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSuggestions(standard) {
|
function getSuggestions(standard, t) {
|
||||||
const base = [
|
const base = [
|
||||||
`What are the key requirements of ${standard.standard_id}?`,
|
t("modal.suggestion_keyReq", { id: standard.standard_id }),
|
||||||
"What materials or tests are specified?",
|
t("modal.suggestion_materials"),
|
||||||
"What are the delivery or packaging specifications?",
|
t("modal.suggestion_delivery"),
|
||||||
];
|
];
|
||||||
if (standard.key_sections?.["Chemical Requirements"]) {
|
if (standard.key_sections?.["Chemical Requirements"]) {
|
||||||
base.splice(1, 0, "Summarise the chemical requirements.");
|
base.splice(1, 0, t("modal.suggestion_chemical"));
|
||||||
}
|
}
|
||||||
if (standard.key_sections?.["Physical Requirements"] || standard.key_sections?.["Physical Requirement"]) {
|
if (standard.key_sections?.["Physical Requirements"] || standard.key_sections?.["Physical Requirement"]) {
|
||||||
base.splice(1, 0, "What are the physical requirements?");
|
base.splice(1, 0, t("modal.suggestion_physical"));
|
||||||
}
|
}
|
||||||
return base.slice(0, 3);
|
return base.slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
|
import enTranslation from "./locales/en/translation.json";
|
||||||
|
import hiTranslation from "./locales/hi/translation.json";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources: {
|
||||||
|
en: { translation: enTranslation },
|
||||||
|
hi: { translation: hiTranslation },
|
||||||
|
},
|
||||||
|
fallbackLng: "en",
|
||||||
|
supportedLngs: ["en", "hi"],
|
||||||
|
interpolation: { escapeValue: false }, // React handles XSS
|
||||||
|
detection: {
|
||||||
|
order: ["localStorage", "navigator"],
|
||||||
|
caches: ["localStorage"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -1,3 +1,28 @@
|
|||||||
|
/* ── Skip navigation (accessibility) ── */
|
||||||
|
.skip-nav {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
top: auto;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.skip-nav:focus {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--accent, #d4530a);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0 0 4px 0;
|
||||||
|
outline: 3px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── BIS SP-21 Design Tokens ── */
|
/* ── BIS SP-21 Design Tokens ── */
|
||||||
:root {
|
:root {
|
||||||
--accent: #d4530a;
|
--accent: #d4530a;
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"brand": "BIS SP\u201121",
|
||||||
|
"standards": "Standards",
|
||||||
|
"categories": "Categories",
|
||||||
|
"recommend": "\u2746 AI Recommend",
|
||||||
|
"about": "About",
|
||||||
|
"bisPortal": "BIS Portal \u2197",
|
||||||
|
"openMenu": "Open menu",
|
||||||
|
"closeMenu": "Close menu"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"brand": "BIS SP\u201121",
|
||||||
|
"tagline": "Handbook on Building Materials\nSpecial Publication 21 : 2005",
|
||||||
|
"portal": "Portal",
|
||||||
|
"searchStandards": "Search Standards",
|
||||||
|
"browseCategories": "Browse Categories",
|
||||||
|
"about": "About",
|
||||||
|
"bis": "Bureau of Indian Standards",
|
||||||
|
"bisWebsite": "BIS Official Website",
|
||||||
|
"manakOnline": "Manak Online",
|
||||||
|
"standardsPortal": "Standards Portal",
|
||||||
|
"ministry": "Ministry",
|
||||||
|
"dpiit": "DPIIT",
|
||||||
|
"nationalPortal": "National Portal",
|
||||||
|
"copyright": "\u00a9 Bureau of Indian Standards, Ministry of Commerce & Industry, Government of India. All rights reserved.",
|
||||||
|
"sourceNote": "Content sourced from BIS Special Publication 21 : 2005. For official standards, refer to"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"eyebrow": "Special Publication 21 \u00b7 2005",
|
||||||
|
"heroTitle": "Handbook of\nBuilding Materials",
|
||||||
|
"heroLead": "Indian Standards across 25 material categories \u2014 searchable, categorised, and ready to reference.",
|
||||||
|
"searchPlaceholder": "Search standards, e.g. Portland Cement, IS 269\u2026",
|
||||||
|
"searchLabel": "Search standards",
|
||||||
|
"searchBtn": "Search",
|
||||||
|
"statsLabel": "Key statistics",
|
||||||
|
"statStandards": "IS Standards",
|
||||||
|
"statCategories": "Categories",
|
||||||
|
"statPages": "Pages Indexed",
|
||||||
|
"categoriesHeading": "25 Material Categories",
|
||||||
|
"categoriesLead": "Every building material section from SP\u201121, indexed and searchable.",
|
||||||
|
"aboutEyebrow": "About SP\u201121",
|
||||||
|
"aboutHeading": "India\u2019s Reference for Building Material Standards",
|
||||||
|
"aboutBody": "BIS Special Publication 21 consolidates all Indian Standards relevant to building and construction materials \u2014 from Portland cement to wire ropes, sanitary fittings to structural steels. Published by the Bureau of Indian Standards, it is the authoritative handbook used by architects, structural engineers, contractors, and quality inspectors across India.",
|
||||||
|
"visitBIS": "Visit BIS Portal \u2197",
|
||||||
|
"pillar_instantRetrieval_title": "Instant Retrieval",
|
||||||
|
"pillar_instantRetrieval_body": "Full-text search across all 573 standards with ranked results.",
|
||||||
|
"pillar_sectionDetail_title": "Section-Level Detail",
|
||||||
|
"pillar_sectionDetail_body": "Scope, requirements, delivery conditions \u2014 all structured fields.",
|
||||||
|
"pillar_categories_title": "25 Categories",
|
||||||
|
"pillar_categories_body": "Organised by BIS material sections, mirroring SP\u201121\u2019s own structure.",
|
||||||
|
"pillar_officialSource_title": "Official Source",
|
||||||
|
"pillar_officialSource_body": "Parsed directly from the BIS SP\u201121 : 2005 authoritative edition.",
|
||||||
|
"standardCount_one": "{{count}} standard",
|
||||||
|
"standardCount_other": "{{count}} standards"
|
||||||
|
},
|
||||||
|
"standards": {
|
||||||
|
"heading": "Find an IS Standard",
|
||||||
|
"lead": "Search by standard number, title, material, or keyword.",
|
||||||
|
"searchPlaceholder": "e.g. Ordinary Portland Cement, IS 269, aggregates\u2026",
|
||||||
|
"searchLabel": "Search standards",
|
||||||
|
"clearSearch": "Clear search",
|
||||||
|
"allCategories": "All Categories",
|
||||||
|
"categoryFilter": "Filter by category",
|
||||||
|
"searching": "Searching\u2026",
|
||||||
|
"found_one": "{{count}} standard found",
|
||||||
|
"found_other": "{{count}} standards found",
|
||||||
|
"page": "page {{page}} of {{total}}",
|
||||||
|
"noResultsTitle": "No standards found",
|
||||||
|
"noResultsSub": "Try a different keyword or clear the category filter.",
|
||||||
|
"pagination": "Results pagination",
|
||||||
|
"prevPage": "Previous page",
|
||||||
|
"nextPage": "Next page",
|
||||||
|
"pageLabel": "Page {{page}}",
|
||||||
|
"serverError": "Could not load standards. Is the server running?"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"eyebrow": "SP\u201121 : 2005",
|
||||||
|
"heading": "Material Categories",
|
||||||
|
"lead_one": "{{total}} standards across {{count}} building material section.",
|
||||||
|
"lead_other": "{{total}} standards across {{count}} building material sections.",
|
||||||
|
"standardCount": "{{count}} standards",
|
||||||
|
"allLabel": "All categories"
|
||||||
|
},
|
||||||
|
"recommend": {
|
||||||
|
"eyebrow": "Hybrid Retrieval \u00b7 AI Explanation",
|
||||||
|
"heading": "Find & Understand Standards",
|
||||||
|
"lead": "Ask a natural language question \u2014 the system retrieves the most relevant IS standards using dense + sparse search, then explains each in plain English.",
|
||||||
|
"searchPlaceholder": "e.g. What standard covers tensile strength of structural steel?",
|
||||||
|
"searchLabel": "Search query",
|
||||||
|
"clearBtn": "Clear",
|
||||||
|
"rewriteLabel": "Smart query rewrite",
|
||||||
|
"rewriteHint": "AI refines your query before searching",
|
||||||
|
"submitBtn": "Find Standards",
|
||||||
|
"submitting": "Searching\u2026",
|
||||||
|
"exampleLabel": "Try an example:",
|
||||||
|
"loadingRetrieval": "Running hybrid retrieval (FAISS + BM25)\u2026",
|
||||||
|
"loadingAI": "Generating AI explanations\u2026",
|
||||||
|
"resultsFound_one": "{{count}} Standard Found",
|
||||||
|
"resultsFound_other": "{{count}} Standards Found",
|
||||||
|
"resultsFor": "for:",
|
||||||
|
"timingLabel": "Timing breakdown",
|
||||||
|
"retrieval": "Retrieval",
|
||||||
|
"ai": "AI",
|
||||||
|
"total": "Total",
|
||||||
|
"rankLabel": "Rank {{rank}}: {{id}}",
|
||||||
|
"section": "\u00a7 {{section}}",
|
||||||
|
"aiExplanation": "AI explanation",
|
||||||
|
"keywords": "Keywords",
|
||||||
|
"relevanceScore": "Relevance score {{score}}",
|
||||||
|
"viewStandard": "View standard details",
|
||||||
|
"error_prefix": "Error:"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"eyebrow": "Bureau of Indian Standards",
|
||||||
|
"heading": "About BIS SP\u201121",
|
||||||
|
"lead": "India\u2019s authoritative handbook on building and construction material standards.",
|
||||||
|
"aboutLabel": "About the publication",
|
||||||
|
"whatTitle": "What is SP\u201121?",
|
||||||
|
"whatBody1": "BIS Special Publication 21 \u2014 Handbook on Building Materials \u2014 is a consolidated reference published by the Bureau of Indian Standards. It brings together all Indian Standards relevant to construction and building materials into a single, organised document.",
|
||||||
|
"whatBody2": "The 2005 edition (the basis of this portal) spans 929 pages across 25 material sections, covering everything from cement and structural steel to timber, paints, sanitary fittings, wire ropes, and thermal insulation.",
|
||||||
|
"whoTitle": "Who uses it?",
|
||||||
|
"whoBody": "SP\u201121 is used daily by structural engineers specifying materials, architects selecting finishes, contractors verifying supplier compliance, quality inspectors conducting audits, and procurement officers evaluating bids. It is the single source of truth for which IS standard governs a given building product.",
|
||||||
|
"portalTitle": "About this portal",
|
||||||
|
"portalBody1": "This portal parses the SP\u201121 : 2005 source document into 573 discrete IS standards with structured fields \u2014 standard ID, title, material category, scope summary, key sections (Requirements, Delivery, Manufacture, etc.), and TF-IDF keywords. Every record is full-text searchable and filterable by category.",
|
||||||
|
"portalBody2": "The parser uses a two-pass boundary detection algorithm to split the PDF\u2019s continuous text into individual standards, with deduplication, section normalisation, and contamination detection to ensure clean, reliable data.",
|
||||||
|
"sidebarPubDetails": "Publication Details",
|
||||||
|
"publisher": "Publisher",
|
||||||
|
"publisherValue": "Bureau of Indian Standards",
|
||||||
|
"edition": "Edition",
|
||||||
|
"editionValue": "SP 21 : 2005",
|
||||||
|
"pages": "Pages",
|
||||||
|
"pagesValue": "929",
|
||||||
|
"standardsIndexed": "Standards indexed",
|
||||||
|
"standardsIndexedValue": "573",
|
||||||
|
"categoriesLabel": "Categories",
|
||||||
|
"categoriesValue": "25",
|
||||||
|
"ministry": "Ministry",
|
||||||
|
"ministryValue": "DPIIT, Govt. of India",
|
||||||
|
"officialLinks": "Official Links",
|
||||||
|
"bisWebsite": "BIS Official Website",
|
||||||
|
"manakOnline": "Manak Online",
|
||||||
|
"standardsPortal": "Standards Portal",
|
||||||
|
"dpiit": "DPIIT"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"closeLabel": "Close standard detail",
|
||||||
|
"summary": "Summary",
|
||||||
|
"keywords": "Keywords",
|
||||||
|
"keySections": "Key Sections",
|
||||||
|
"askAI": "Ask AI about this standard",
|
||||||
|
"conversation": "Conversation",
|
||||||
|
"aiResponse": "AI response",
|
||||||
|
"aiThinking": "AI is thinking",
|
||||||
|
"suggestionsLabel": "Suggested questions",
|
||||||
|
"chatPlaceholder": "Ask a question about this standard\u2026",
|
||||||
|
"questionLabel": "Your question",
|
||||||
|
"sendLabel": "Send question",
|
||||||
|
"sending": "\u2026",
|
||||||
|
"askBtn": "Ask",
|
||||||
|
"notFound": "No content found for this standard.",
|
||||||
|
"suggestion_keyReq": "What are the key requirements of {{id}}?",
|
||||||
|
"suggestion_materials": "What materials or tests are specified?",
|
||||||
|
"suggestion_delivery": "What are the delivery or packaging specifications?",
|
||||||
|
"suggestion_chemical": "Summarise the chemical requirements.",
|
||||||
|
"suggestion_physical": "What are the physical requirements?"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"viewDetails": "View details for {{id}}",
|
||||||
|
"section_one": "{{count}} section",
|
||||||
|
"section_other": "{{count}} sections",
|
||||||
|
"keywords": "Keywords"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"serverError": "Could not connect to the server.",
|
||||||
|
"loading": "Loading\u2026"
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"en": "English",
|
||||||
|
"hi": "\u0939\u093f\u0928\u094d\u0926\u0940",
|
||||||
|
"switchTo": "Switch language"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"brand": "BIS SP\u201121",
|
||||||
|
"standards": "\u092e\u093e\u0928\u0915",
|
||||||
|
"categories": "\u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"recommend": "\u2746 AI \u0905\u0928\u0941\u0936\u0902\u0938\u093e",
|
||||||
|
"about": "\u0939\u092e\u093e\u0930\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902",
|
||||||
|
"bisPortal": "BIS \u092a\u094b\u0930\u094d\u091f\u0932 \u2197",
|
||||||
|
"openMenu": "\u092e\u0947\u0928\u0942 \u0916\u094b\u0932\u0947\u0902",
|
||||||
|
"closeMenu": "\u092e\u0947\u0928\u0942 \u092c\u0902\u0926 \u0915\u0930\u0947\u0902"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"brand": "BIS SP\u201121",
|
||||||
|
"tagline": "\u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092a\u0941\u0938\u094d\u0924\u093f\u0915\u093e\nSP 21 : 2005",
|
||||||
|
"portal": "\u092a\u094b\u0930\u094d\u091f\u0932",
|
||||||
|
"searchStandards": "\u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902",
|
||||||
|
"browseCategories": "\u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901 \u0926\u0947\u0916\u0947\u0902",
|
||||||
|
"about": "\u0939\u092e\u093e\u0930\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902",
|
||||||
|
"bis": "\u092d\u093e\u0930\u0924\u0940\u092f \u092e\u093e\u0928\u0915 \u092c\u094d\u092f\u0942\u0930\u094b",
|
||||||
|
"bisWebsite": "BIS \u0906\u0927\u093f\u0915\u093e\u0930\u093f\u0915 \u0935\u0947\u092c\u0938\u093e\u0907\u091f",
|
||||||
|
"manakOnline": "\u092e\u093e\u0928\u0915 \u0911\u0928\u0932\u093e\u0907\u0928",
|
||||||
|
"standardsPortal": "\u092e\u093e\u0928\u0915 \u092a\u094b\u0930\u094d\u091f\u0932",
|
||||||
|
"ministry": "\u092e\u0902\u0924\u094d\u0930\u093e\u0932\u092f",
|
||||||
|
"dpiit": "DPIIT",
|
||||||
|
"nationalPortal": "\u0930\u093e\u0937\u094d\u091f\u094d\u0930\u0940\u092f \u092a\u094b\u0930\u094d\u091f\u0932",
|
||||||
|
"copyright": "\u00a9 \u092d\u093e\u0930\u0924\u0940\u092f \u092e\u093e\u0928\u0915 \u092c\u094d\u092f\u0942\u0930\u094b, \u0935\u093e\u0923\u093f\u091c\u094d\u092f \u090f\u0935\u0902 \u0909\u0926\u094d\u092f\u094b\u0917 \u092e\u0902\u0924\u094d\u0930\u093e\u0932\u092f, \u092d\u093e\u0930\u0924 \u0938\u0930\u0915\u093e\u0930\u0964 \u0938\u0930\u094d\u0935\u093e\u0927\u093f\u0915\u093e\u0930 \u0938\u0941\u0930\u0915\u094d\u0937\u093f\u0924\u0964",
|
||||||
|
"sourceNote": "BIS SP 21 : 2005 \u0938\u0947 \u0938\u0902\u0915\u0932\u093f\u0924\u0964 \u0906\u0927\u093f\u0915\u093e\u0930\u093f\u0915 \u092e\u093e\u0928\u0915\u094b\u0902 \u0915\u0947 \u0932\u093f\u090f \u0926\u0947\u0916\u0947\u0902"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"eyebrow": "\u0935\u093f\u0936\u0947\u0937 \u092a\u094d\u0930\u0915\u093e\u0936\u0928 21 \u00b7 2005",
|
||||||
|
"heroTitle": "\u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940\u0915\u093e \u0939\u0938\u094d\u0924\u092a\u0941\u0938\u094d\u0924\u093f\u0915\u093e",
|
||||||
|
"heroLead": "25 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0936\u094d\u0930\u0947\u0923\u093f\u092f\u094b\u0902 \u092e\u0947\u0902 \u092d\u093e\u0930\u0924\u0940\u092f \u092e\u093e\u0928\u0915 \u2014 \u0916\u094b\u091c\u092f\u094b\u0917\u094d\u092f, \u0935\u0930\u094d\u0917\u0940\u0915\u0943\u0924 \u0914\u0930 \u0938\u0902\u0926\u0930\u094d\u092d \u0915\u0947 \u0932\u093f\u090f \u0924\u0948\u092f\u093e\u0930\u0964",
|
||||||
|
"searchPlaceholder": "\u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902, \u091c\u0948\u0938\u0947 Portland Cement, IS 269\u2026",
|
||||||
|
"searchLabel": "\u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902",
|
||||||
|
"searchBtn": "\u0916\u094b\u091c\u0947\u0902",
|
||||||
|
"statsLabel": "\u092e\u0941\u0916\u094d\u092f \u0906\u0902\u0915\u095c\u0947",
|
||||||
|
"statStandards": "IS \u092e\u093e\u0928\u0915",
|
||||||
|
"statCategories": "\u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"statPages": "\u0938\u0942\u091a\u0940\u092c\u0926\u094d\u0927 \u092a\u0943\u0937\u094d\u0920",
|
||||||
|
"categoriesHeading": "25 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"categoriesLead": "SP\u201121 \u0915\u0940 \u0939\u0930 \u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0936\u094d\u0930\u0947\u0923\u0940, \u0938\u0942\u091a\u0940\u092c\u0926\u094d\u0927 \u0914\u0930 \u0916\u094b\u091c\u092f\u094b\u0917\u094d\u092f\u0964",
|
||||||
|
"aboutEyebrow": "SP\u201121 \u0915\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902",
|
||||||
|
"aboutHeading": "\u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092e\u093e\u0928\u0915\u094b\u0902 \u0915\u093e \u092d\u093e\u0930\u0924\u0940\u092f \u0938\u0902\u0926\u0930\u094d\u092d",
|
||||||
|
"aboutBody": "BIS \u0935\u093f\u0936\u0947\u0937 \u092a\u094d\u0930\u0915\u093e\u0936\u0928 21 \u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0914\u0930 \u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u093f\u092f\u094b\u0902 \u0938\u0947 \u0938\u0902\u092c\u0902\u0927\u093f\u0924 \u0938\u092d\u0940 IS \u092e\u093e\u0928\u0915\u094b\u0902 \u0915\u094b \u090f\u0915\u0924\u094d\u0930 \u0915\u0930\u0924\u093e \u0939\u0948\u0964",
|
||||||
|
"visitBIS": "BIS \u092a\u094b\u0930\u094d\u091f\u0932 \u0926\u0947\u0916\u0947\u0902 \u2197",
|
||||||
|
"pillar_instantRetrieval_title": "\u0924\u0924\u094d\u0915\u093e\u0932 \u0916\u094b\u091c",
|
||||||
|
"pillar_instantRetrieval_body": "\u0938\u092d\u0940 573 \u092e\u093e\u0928\u0915\u094b\u0902 \u092e\u0947\u0902 \u092a\u0942\u0930\u094d\u0923-\u092a\u093e\u0920 \u0916\u094b\u091c \u0930\u0948\u0902\u0915\u093f\u0902\u0917 \u092a\u0930\u093f\u0923\u093e\u092e\u094b\u0902 \u0915\u0947 \u0938\u093e\u0925\u0964",
|
||||||
|
"pillar_sectionDetail_title": "\u0927\u093e\u0930\u093e-\u0938\u094d\u0924\u0930\u0940\u092f \u0935\u093f\u0935\u0930\u0923",
|
||||||
|
"pillar_sectionDetail_body": "\u0915\u094d\u0937\u0947\u0924\u094d\u0930, \u0906\u0935\u0936\u094d\u092f\u0915\u0924\u093e\u090f\u0901, \u0921\u093f\u0932\u0940\u0935\u0930\u0940 \u0936\u0930\u094d\u0924\u0947\u0902 \u2014 \u0938\u092d\u0940 \u0938\u0902\u0930\u091a\u093f\u0924 \u092b\u093c\u0940\u0932\u094d\u0921\u0964",
|
||||||
|
"pillar_categories_title": "25 \u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"pillar_categories_body": "BIS \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0916\u0902\u0921\u094b\u0902 \u0926\u094d\u0935\u093e\u0930\u093e \u0935\u094d\u092f\u0935\u0938\u094d\u0925\u093f\u0924, SP\u201121 \u0915\u0940 \u0905\u092a\u0928\u0940 \u0938\u0902\u0930\u091a\u0928\u093e \u0915\u093e \u0905\u0928\u0941\u0938\u0930\u0923\u0964",
|
||||||
|
"pillar_officialSource_title": "\u0906\u0927\u093f\u0915\u093e\u0930\u093f\u0915 \u0938\u094d\u0930\u094b\u0924",
|
||||||
|
"pillar_officialSource_body": "BIS SP\u201121 : 2005 \u0938\u0947 \u0938\u0940\u0927\u0947 \u092a\u093e\u0930\u094d\u0938 \u0915\u093f\u092f\u093e \u0917\u092f\u093e\u0964",
|
||||||
|
"standardCount_one": "{{count}} \u092e\u093e\u0928\u0915",
|
||||||
|
"standardCount_other": "{{count}} \u092e\u093e\u0928\u0915"
|
||||||
|
},
|
||||||
|
"standards": {
|
||||||
|
"heading": "IS \u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902",
|
||||||
|
"lead": "\u092e\u093e\u0928\u0915 \u0938\u0902\u0916\u094d\u092f\u093e, \u0936\u0940\u0930\u094d\u0937\u0915, \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092f\u093e \u0915\u0940\u0935\u0930\u094d\u0921 \u0926\u094d\u0935\u093e\u0930\u093e \u0916\u094b\u091c\u0947\u0902\u0964",
|
||||||
|
"searchPlaceholder": "\u091c\u0948\u0938\u0947 Ordinary Portland Cement, IS 269\u2026",
|
||||||
|
"searchLabel": "\u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902",
|
||||||
|
"clearSearch": "\u0916\u094b\u091c \u0938\u093e\u092b\u093c \u0915\u0930\u0947\u0902",
|
||||||
|
"allCategories": "\u0938\u092d\u0940 \u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"categoryFilter": "\u0936\u094d\u0930\u0947\u0923\u0940 \u0938\u0947 \u092b\u093c\u093f\u0932\u094d\u091f\u0930 \u0915\u0930\u0947\u0902",
|
||||||
|
"searching": "\u0916\u094b\u091c \u091c\u093e\u0930\u0940\u2026",
|
||||||
|
"found_one": "{{count}} \u092e\u093e\u0928\u0915 \u092e\u093f\u0932\u093e",
|
||||||
|
"found_other": "{{count}} \u092e\u093e\u0928\u0915 \u092e\u093f\u0932\u0947",
|
||||||
|
"page": "\u092a\u0943\u0937\u094d\u0920 {{page}} / {{total}}",
|
||||||
|
"noResultsTitle": "\u0915\u094b\u0908 \u092e\u093e\u0928\u0915 \u0928\u0939\u0940\u0902 \u092e\u093f\u0932\u093e",
|
||||||
|
"noResultsSub": "\u0905\u0932\u0917 \u0915\u0940\u0935\u0930\u094d\u0921 \u0906\u091c\u093c\u092e\u093e\u090f\u0901 \u092f\u093e \u0936\u094d\u0930\u0947\u0923\u0940 \u092b\u093c\u093f\u0932\u094d\u091f\u0930 \u0939\u091f\u093e\u090f\u0902\u0964",
|
||||||
|
"pagination": "\u092a\u0930\u093f\u0923\u093e\u092e \u092a\u0943\u0937\u094d\u0920\u093e\u0902\u0915\u0928",
|
||||||
|
"prevPage": "\u092a\u093f\u091b\u0932\u093e \u092a\u0943\u0937\u094d\u0920",
|
||||||
|
"nextPage": "\u0905\u0917\u0932\u093e \u092a\u0943\u0937\u094d\u0920",
|
||||||
|
"pageLabel": "\u092a\u0943\u0937\u094d\u0920 {{page}}",
|
||||||
|
"serverError": "\u092e\u093e\u0928\u0915 \u0932\u094b\u0921 \u0928\u0939\u0940\u0902 \u0939\u094b \u0938\u0915\u0947\u0964 \u0915\u094d\u092f\u093e \u0938\u0930\u094d\u0935\u0930 \u091a\u0932 \u0930\u0939\u093e \u0939\u0948?"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"eyebrow": "SP\u201121 : 2005",
|
||||||
|
"heading": "\u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"lead_one": "{{count}} \u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0916\u0902\u0921 \u092e\u0947\u0902 {{total}} \u092e\u093e\u0928\u0915\u0964",
|
||||||
|
"lead_other": "{{count}} \u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0916\u0902\u0921\u094b\u0902 \u092e\u0947\u0902 {{total}} \u092e\u093e\u0928\u0915\u0964",
|
||||||
|
"standardCount": "{{count}} \u092e\u093e\u0928\u0915",
|
||||||
|
"allLabel": "\u0938\u092d\u0940 \u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901"
|
||||||
|
},
|
||||||
|
"recommend": {
|
||||||
|
"eyebrow": "\u0939\u093e\u0907\u092c\u094d\u0930\u093f\u0921 \u092a\u0941\u0928\u0930\u094d\u092a\u094d\u0930\u093e\u092a\u094d\u0924\u093f \u00b7 AI \u0935\u094d\u092f\u093e\u0916\u094d\u092f\u093e",
|
||||||
|
"heading": "\u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902 \u0914\u0930 \u0938\u092e\u091d\u0947\u0902",
|
||||||
|
"lead": "\u092a\u094d\u0930\u093e\u0915\u0943\u0924\u093f\u0915 \u092d\u093e\u0937\u093e \u092e\u0947\u0902 \u092a\u0942\u091b\u0947\u0902 \u2014 \u0938\u093f\u0938\u094d\u091f\u092e dense + sparse \u0916\u094b\u091c \u0938\u0947 \u0938\u092c\u0938\u0947 \u092a\u094d\u0930\u093e\u0938\u0902\u0917\u093f\u0915 IS \u092e\u093e\u0928\u0915 \u0922\u0942\u0902\u0922\u0924\u093e \u0939\u0948\u0964",
|
||||||
|
"searchPlaceholder": "\u091c\u0948\u0938\u0947: \u0938\u0902\u0930\u091a\u0928\u093e\u0924\u094d\u092e\u0915 \u0938\u094d\u091f\u0940\u0932 \u0915\u0940 \u0924\u0928\u094d\u092f \u0936\u0915\u094d\u0924\u093f \u0915\u093e \u092e\u093e\u0928\u0915 \u0915\u094c\u0928 \u0938\u093e \u0939\u0948?",
|
||||||
|
"searchLabel": "\u0916\u094b\u091c \u092a\u094d\u0930\u0936\u094d\u0928",
|
||||||
|
"clearBtn": "\u0938\u093e\u092b\u093c \u0915\u0930\u0947\u0902",
|
||||||
|
"rewriteLabel": "\u0938\u094d\u092e\u093e\u0930\u094d\u091f \u0915\u094d\u0935\u0947\u0930\u0940 \u0930\u093f\u0930\u093e\u0907\u091f",
|
||||||
|
"rewriteHint": "AI \u0916\u094b\u091c \u0938\u0947 \u092a\u0939\u0932\u0947 \u0906\u092a\u0915\u0940 \u0915\u094d\u0935\u0947\u0930\u0940 \u0938\u0941\u0927\u093e\u0930\u0924\u093e \u0939\u0948",
|
||||||
|
"submitBtn": "\u092e\u093e\u0928\u0915 \u0916\u094b\u091c\u0947\u0902",
|
||||||
|
"submitting": "\u0916\u094b\u091c \u091c\u093e\u0930\u0940\u2026",
|
||||||
|
"exampleLabel": "\u0909\u0926\u093e\u0939\u0930\u0923 \u0906\u091c\u093c\u092e\u093e\u090f\u0901:",
|
||||||
|
"loadingRetrieval": "\u0939\u093e\u0907\u092c\u094d\u0930\u093f\u0921 \u0916\u094b\u091c \u091a\u0932 \u0930\u0939\u0940 \u0939\u0948 (FAISS + BM25)\u2026",
|
||||||
|
"loadingAI": "AI \u0935\u094d\u092f\u093e\u0916\u094d\u092f\u093e \u0924\u0948\u092f\u093e\u0930 \u0939\u094b \u0930\u0939\u0940 \u0939\u0948\u2026",
|
||||||
|
"resultsFound_one": "{{count}} \u092e\u093e\u0928\u0915 \u092e\u093f\u0932\u093e",
|
||||||
|
"resultsFound_other": "{{count}} \u092e\u093e\u0928\u0915 \u092e\u093f\u0932\u0947",
|
||||||
|
"resultsFor": "\u0916\u094b\u091c:",
|
||||||
|
"timingLabel": "\u0938\u092e\u092f \u0935\u093f\u0935\u0930\u0923",
|
||||||
|
"retrieval": "\u0916\u094b\u091c",
|
||||||
|
"ai": "AI",
|
||||||
|
"total": "\u0915\u0941\u0932",
|
||||||
|
"rankLabel": "\u0930\u0948\u0902\u0915 {{rank}}: {{id}}",
|
||||||
|
"section": "\u00a7 {{section}}",
|
||||||
|
"aiExplanation": "AI \u0935\u094d\u092f\u093e\u0916\u094d\u092f\u093e",
|
||||||
|
"keywords": "\u0915\u0940\u0935\u0930\u094d\u0921",
|
||||||
|
"relevanceScore": "\u092a\u094d\u0930\u093e\u0938\u0902\u0917\u093f\u0915\u0924\u093e \u0938\u094d\u0915\u094b\u0930 {{score}}",
|
||||||
|
"viewStandard": "\u092e\u093e\u0928\u0915 \u0935\u093f\u0935\u0930\u0923 \u0926\u0947\u0916\u0947\u0902",
|
||||||
|
"error_prefix": "\u0924\u094d\u0930\u0941\u091f\u093f:"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"eyebrow": "\u092d\u093e\u0930\u0924\u0940\u092f \u092e\u093e\u0928\u0915 \u092c\u094d\u092f\u0942\u0930\u094b",
|
||||||
|
"heading": "BIS SP\u201121 \u0915\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902",
|
||||||
|
"lead": "\u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092e\u093e\u0928\u0915\u094b\u0902 \u092a\u0930 \u092d\u093e\u0930\u0924 \u0915\u0940 \u0906\u0927\u093f\u0915\u093e\u0930\u093f\u0915 \u092a\u0941\u0938\u094d\u0924\u093f\u0915\u093e\u0964",
|
||||||
|
"aboutLabel": "\u092a\u094d\u0930\u0915\u093e\u0936\u0928 \u0915\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902",
|
||||||
|
"whatTitle": "SP\u201121 \u0915\u094d\u092f\u093e \u0939\u0948?",
|
||||||
|
"whatBody1": "BIS \u0935\u093f\u0936\u0947\u0937 \u092a\u094d\u0930\u0915\u093e\u0936\u0928 21 \u2014 \u0928\u093f\u0930\u094d\u092e\u093e\u0923 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092a\u0941\u0938\u094d\u0924\u093f\u0915\u093e \u2014 \u092d\u093e\u0930\u0924\u0940\u092f \u092e\u093e\u0928\u0915 \u092c\u094d\u092f\u0942\u0930\u094b \u0926\u094d\u0935\u093e\u0930\u093e \u092a\u094d\u0930\u0915\u093e\u0936\u093f\u0924 \u090f\u0915 \u0938\u0902\u092f\u0941\u0915\u094d\u0924 \u0938\u0902\u0926\u0930\u094d\u092d \u0939\u0948\u0964",
|
||||||
|
"whatBody2": "2005 \u0938\u0902\u0938\u094d\u0915\u0930\u0923 25 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0916\u0902\u0921\u094b\u0902 \u092e\u0947\u0902 929 \u092a\u0943\u0937\u094d\u0920 \u0915\u0935\u0930 \u0915\u0930\u0924\u093e \u0939\u0948\u0964",
|
||||||
|
"whoTitle": "\u0907\u0938\u0947 \u0915\u094c\u0928 \u0909\u092a\u092f\u094b\u0917 \u0915\u0930\u0924\u093e \u0939\u0948?",
|
||||||
|
"whoBody": "SP\u201121 \u0915\u093e \u0909\u092a\u092f\u094b\u0917 \u0938\u0902\u0930\u091a\u0928\u093e\u0924\u094d\u092e\u0915 \u0907\u0902\u091c\u0940\u0928\u093f\u092f\u0930, \u0935\u093e\u0938\u094d\u0924\u0941\u0915\u093e\u0930, \u0920\u0947\u0915\u0947\u0926\u093e\u0930, \u0917\u0941\u0923\u0935\u0924\u094d\u0924\u093e \u0928\u093f\u0930\u0940\u0915\u094d\u0937\u0915 \u0914\u0930 \u0916\u0930\u0940\u0926 \u0905\u0927\u093f\u0915\u093e\u0930\u0940 \u0915\u0930\u0924\u0947 \u0939\u0948\u0902\u0964",
|
||||||
|
"portalTitle": "\u0907\u0938 \u092a\u094b\u0930\u094d\u091f\u0932 \u0915\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902",
|
||||||
|
"portalBody1": "\u092f\u0939 \u092a\u094b\u0930\u094d\u091f\u0932 SP\u201121 : 2005 \u0915\u094b 573 IS \u092e\u093e\u0928\u0915\u094b\u0902 \u092e\u0947\u0902 \u092a\u093e\u0930\u094d\u0938 \u0915\u0930\u0924\u093e \u0939\u0948\u0964",
|
||||||
|
"portalBody2": "\u092a\u093e\u0930\u094d\u0938\u0930 \u0926\u094b-\u092a\u093e\u0938 \u0938\u0940\u092e\u093e \u0928\u093f\u0930\u094d\u0927\u093e\u0930\u0923 \u090f\u0932\u094d\u0917\u094b\u0930\u093f\u0926\u092e \u0915\u093e \u0909\u092a\u092f\u094b\u0917 \u0915\u0930\u0924\u093e \u0939\u0948\u0964",
|
||||||
|
"sidebarPubDetails": "\u092a\u094d\u0930\u0915\u093e\u0936\u0928 \u0935\u093f\u0935\u0930\u0923",
|
||||||
|
"publisher": "\u092a\u094d\u0930\u0915\u093e\u0936\u0915",
|
||||||
|
"publisherValue": "\u092d\u093e\u0930\u0924\u0940\u092f \u092e\u093e\u0928\u0915 \u092c\u094d\u092f\u0942\u0930\u094b",
|
||||||
|
"edition": "\u0938\u0902\u0938\u094d\u0915\u0930\u0923",
|
||||||
|
"editionValue": "SP 21 : 2005",
|
||||||
|
"pages": "\u092a\u0943\u0937\u094d\u0920",
|
||||||
|
"pagesValue": "929",
|
||||||
|
"standardsIndexed": "\u0938\u0942\u091a\u0940\u092c\u0926\u094d\u0927 \u092e\u093e\u0928\u0915",
|
||||||
|
"standardsIndexedValue": "573",
|
||||||
|
"categoriesLabel": "\u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0901",
|
||||||
|
"categoriesValue": "25",
|
||||||
|
"ministry": "\u092e\u0902\u0924\u094d\u0930\u093e\u0932\u092f",
|
||||||
|
"ministryValue": "DPIIT, \u092d\u093e\u0930\u0924 \u0938\u0930\u0915\u093e\u0930",
|
||||||
|
"officialLinks": "\u0906\u0927\u093f\u0915\u093e\u0930\u093f\u0915 \u0932\u093f\u0902\u0915",
|
||||||
|
"bisWebsite": "BIS \u0906\u0927\u093f\u0915\u093e\u0930\u093f\u0915 \u0935\u0947\u092c\u0938\u093e\u0907\u091f",
|
||||||
|
"manakOnline": "\u092e\u093e\u0928\u0915 \u0911\u0928\u0932\u093e\u0907\u0928",
|
||||||
|
"standardsPortal": "\u092e\u093e\u0928\u0915 \u092a\u094b\u0930\u094d\u091f\u0932",
|
||||||
|
"dpiit": "DPIIT"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"closeLabel": "\u092e\u093e\u0928\u0915 \u0935\u093f\u0935\u0930\u0923 \u092c\u0902\u0926 \u0915\u0930\u0947\u0902",
|
||||||
|
"summary": "\u0938\u093e\u0930\u093e\u0902\u0936",
|
||||||
|
"keywords": "\u0915\u0940\u0935\u0930\u094d\u0921",
|
||||||
|
"keySections": "\u092e\u0941\u0916\u094d\u092f \u0927\u093e\u0930\u093e\u090f\u0901",
|
||||||
|
"askAI": "\u0907\u0938 \u092e\u093e\u0928\u0915 \u0915\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902 AI \u0938\u0947 \u092a\u0942\u091b\u0947\u0902",
|
||||||
|
"conversation": "\u0935\u093e\u0930\u094d\u0924\u093e\u0932\u093e\u092a",
|
||||||
|
"aiResponse": "AI \u0909\u0924\u094d\u0924\u0930",
|
||||||
|
"aiThinking": "AI \u0938\u094b\u091a \u0930\u0939\u093e \u0939\u0948",
|
||||||
|
"suggestionsLabel": "\u0938\u0941\u091d\u093e\u090f \u0917\u090f \u092a\u094d\u0930\u0936\u094d\u0928",
|
||||||
|
"chatPlaceholder": "\u0907\u0938 \u092e\u093e\u0928\u0915 \u0915\u0947 \u092c\u093e\u0930\u0947 \u092e\u0947\u0902 \u092a\u094d\u0930\u0936\u094d\u0928 \u092a\u0942\u091b\u0947\u0902\u2026",
|
||||||
|
"questionLabel": "\u0906\u092a\u0915\u093e \u092a\u094d\u0930\u0936\u094d\u0928",
|
||||||
|
"sendLabel": "\u092a\u094d\u0930\u0936\u094d\u0928 \u092d\u0947\u091c\u0947\u0902",
|
||||||
|
"sending": "\u2026",
|
||||||
|
"askBtn": "\u092a\u0942\u091b\u0947\u0902",
|
||||||
|
"notFound": "\u0907\u0938 \u092e\u093e\u0928\u0915 \u092e\u0947\u0902 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0928\u0939\u0940\u0902 \u092e\u093f\u0932\u0940\u0964",
|
||||||
|
"suggestion_keyReq": "{{id}} \u0915\u0940 \u092e\u0941\u0916\u094d\u092f \u0906\u0935\u0936\u094d\u092f\u0915\u0924\u093e\u090f\u0901 \u0915\u094d\u092f\u093e \u0939\u0948\u0902?",
|
||||||
|
"suggestion_materials": "\u0915\u094c\u0928 \u0938\u0940 \u0938\u093e\u092e\u0917\u094d\u0930\u093f\u092f\u093e\u0901 \u092f\u093e \u092a\u0930\u0940\u0915\u094d\u0937\u0923 \u0928\u093f\u0930\u094d\u0926\u093f\u0937\u094d\u091f \u0939\u0948\u0902?",
|
||||||
|
"suggestion_delivery": "\u0921\u093f\u0932\u0940\u0935\u0930\u0940 \u092f\u093e \u092a\u0948\u0915\u0947\u091c\u093f\u0902\u0917 \u0935\u093f\u0936\u093f\u0937\u094d\u091f\u0924\u093e\u090f\u0901 \u0915\u094d\u092f\u093e \u0939\u0948\u0902?",
|
||||||
|
"suggestion_chemical": "\u0930\u093e\u0938\u093e\u092f\u0928\u093f\u0915 \u0906\u0935\u0936\u094d\u092f\u0915\u0924\u093e\u0913\u0902 \u0915\u093e \u0938\u093e\u0930\u093e\u0902\u0936 \u0926\u0947\u0902\u0964",
|
||||||
|
"suggestion_physical": "\u092d\u094c\u0924\u093f\u0915 \u0906\u0935\u0936\u094d\u092f\u0915\u0924\u093e\u090f\u0901 \u0915\u094d\u092f\u093a \u0939\u0948\u0902?"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"viewDetails": "{{id}} \u0915\u093e \u0935\u093f\u0935\u0930\u0923 \u0926\u0947\u0916\u0947\u0902",
|
||||||
|
"section_one": "{{count}} \u0927\u093e\u0930\u093e",
|
||||||
|
"section_other": "{{count}} \u0927\u093e\u0930\u093e\u090f\u0901",
|
||||||
|
"keywords": "\u0915\u0940\u0935\u0930\u094d\u0921"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"serverError": "\u0938\u0930\u094d\u0935\u0930 \u0938\u0947 \u0915\u0928\u0947\u0915\u094d\u091f \u0928\u0939\u0940\u0902 \u0939\u094b \u0938\u0915\u093e\u0964",
|
||||||
|
"loading": "\u0932\u094b\u0921 \u0939\u094b \u0930\u0939\u093e \u0939\u0948\u2026"
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"en": "English",
|
||||||
|
"hi": "\u0939\u093f\u0928\u094d\u0926\u0940",
|
||||||
|
"switchTo": "\u092d\u093e\u0937\u093e \u092c\u0926\u0932\u0947\u0902"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import "./i18n.js";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App.jsx";
|
import App from "./App.jsx";
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,59 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import "./About.css";
|
import "./About.css";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<section className="tile tile-dark about-hero" aria-labelledby="about-heading">
|
<section className="tile tile-dark about-hero" aria-labelledby="about-heading">
|
||||||
<div className="tile-inner tile-center">
|
<div className="tile-inner tile-center">
|
||||||
<p className="tile-eyebrow">Bureau of Indian Standards</p>
|
<p className="tile-eyebrow">{t("about.eyebrow")}</p>
|
||||||
<h1 className="hero-display" id="about-heading">About BIS SP‑21</h1>
|
<h1 className="hero-display" id="about-heading">{t("about.heading")}</h1>
|
||||||
<p className="lead">
|
<p className="lead">{t("about.lead")}</p>
|
||||||
India's authoritative handbook on building and construction material standards.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="tile tile-light" aria-label="About the publication">
|
<section className="tile tile-light" aria-label={t("about.aboutLabel")}>
|
||||||
<div className="tile-inner about-content">
|
<div className="tile-inner about-content">
|
||||||
<div className="about-main">
|
<div className="about-main">
|
||||||
<h2 className="about-section-title">What is SP‑21?</h2>
|
<h2 className="about-section-title">{t("about.whatTitle")}</h2>
|
||||||
<p className="about-body">
|
<p className="about-body">{t("about.whatBody1")}</p>
|
||||||
BIS Special Publication 21 — <em>Handbook on Building Materials</em> — is a consolidated
|
<p className="about-body">{t("about.whatBody2")}</p>
|
||||||
reference published by the Bureau of Indian Standards. It brings together all Indian
|
|
||||||
Standards relevant to construction and building materials into a single, organised document.
|
|
||||||
</p>
|
|
||||||
<p className="about-body">
|
|
||||||
The 2005 edition (the basis of this portal) spans 929 pages across 25 material sections,
|
|
||||||
covering everything from cement and structural steel to timber, paints, sanitary fittings,
|
|
||||||
wire ropes, and thermal insulation.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="about-section-title">Who uses it?</h2>
|
<h2 className="about-section-title">{t("about.whoTitle")}</h2>
|
||||||
<p className="about-body">
|
<p className="about-body">{t("about.whoBody")}</p>
|
||||||
SP‑21 is used daily by structural engineers specifying materials, architects selecting
|
|
||||||
finishes, contractors verifying supplier compliance, quality inspectors conducting audits,
|
|
||||||
and procurement officers evaluating bids. It is the single source of truth for which IS
|
|
||||||
standard governs a given building product.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="about-section-title">About this portal</h2>
|
<h2 className="about-section-title">{t("about.portalTitle")}</h2>
|
||||||
<p className="about-body">
|
<p className="about-body">{t("about.portalBody1")}</p>
|
||||||
This portal parses the SP‑21 : 2005 source document into 573 discrete IS standards with
|
<p className="about-body">{t("about.portalBody2")}</p>
|
||||||
structured fields — standard ID, title, material category, scope summary, key sections
|
|
||||||
(Requirements, Delivery, Manufacture, etc.), and TF-IDF keywords. Every record is
|
|
||||||
full-text searchable and filterable by category.
|
|
||||||
</p>
|
|
||||||
<p className="about-body">
|
|
||||||
The parser uses a two-pass boundary detection algorithm to split the PDF's continuous
|
|
||||||
text into individual standards, with deduplication, section normalisation, and
|
|
||||||
contamination detection to ensure clean, reliable data.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="about-sidebar">
|
<aside className="about-sidebar">
|
||||||
<div className="about-stat-card">
|
<div className="about-stat-card">
|
||||||
<h3 className="sidebar-heading">Publication Details</h3>
|
<h3 className="sidebar-heading">{t("about.sidebarPubDetails")}</h3>
|
||||||
<dl className="detail-list">
|
<dl className="detail-list">
|
||||||
<dt>Publisher</dt><dd>Bureau of Indian Standards</dd>
|
<dt>{t("about.publisher")}</dt><dd>{t("about.publisherValue")}</dd>
|
||||||
<dt>Edition</dt><dd>SP 21 : 2005</dd>
|
<dt>{t("about.edition")}</dt><dd>{t("about.editionValue")}</dd>
|
||||||
<dt>Pages</dt><dd>929</dd>
|
<dt>{t("about.pages")}</dt><dd>{t("about.pagesValue")}</dd>
|
||||||
<dt>Standards indexed</dt><dd>573</dd>
|
<dt>{t("about.standardsIndexed")}</dt><dd>{t("about.standardsIndexedValue")}</dd>
|
||||||
<dt>Categories</dt><dd>25</dd>
|
<dt>{t("about.categoriesLabel")}</dt><dd>{t("about.categoriesValue")}</dd>
|
||||||
<dt>Ministry</dt><dd>DPIIT, Govt. of India</dd>
|
<dt>{t("about.ministry")}</dt><dd>{t("about.ministryValue")}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div className="about-links-card">
|
<div className="about-links-card">
|
||||||
<h3 className="sidebar-heading">Official Links</h3>
|
<h3 className="sidebar-heading">{t("about.officialLinks")}</h3>
|
||||||
<a className="ext-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">
|
<a className="ext-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">
|
||||||
<span>BIS Official Website</span><span aria-hidden="true">↗</span>
|
<span>{t("about.bisWebsite")}</span><span aria-hidden="true">↗</span>
|
||||||
</a>
|
</a>
|
||||||
<a className="ext-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">
|
<a className="ext-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">
|
||||||
<span>Manak Online</span><span aria-hidden="true">↗</span>
|
<span>{t("about.manakOnline")}</span><span aria-hidden="true">↗</span>
|
||||||
</a>
|
</a>
|
||||||
<a className="ext-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">
|
<a className="ext-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">
|
||||||
<span>Standards Portal</span><span aria-hidden="true">↗</span>
|
<span>{t("about.standardsPortal")}</span><span aria-hidden="true">↗</span>
|
||||||
</a>
|
</a>
|
||||||
<a className="ext-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">
|
<a className="ext-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">
|
||||||
<span>DPIIT</span><span aria-hidden="true">↗</span>
|
<span>{t("about.dpiit")}</span><span aria-hidden="true">↗</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { fetchCategories } from "../api/standards";
|
import { fetchCategories } from "../api/standards";
|
||||||
import "./Categories.css";
|
import "./Categories.css";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ const CATEGORY_ICONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Categories() {
|
export default function Categories() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -49,15 +51,15 @@ export default function Categories() {
|
|||||||
<main>
|
<main>
|
||||||
<section className="tile tile-dark cat-hero" aria-labelledby="cat-page-heading">
|
<section className="tile tile-dark cat-hero" aria-labelledby="cat-page-heading">
|
||||||
<div className="tile-inner tile-center">
|
<div className="tile-inner tile-center">
|
||||||
<p className="tile-eyebrow">SP‑21 : 2005</p>
|
<p className="tile-eyebrow">{t("categories.eyebrow")}</p>
|
||||||
<h1 className="hero-display" id="cat-page-heading">Material Categories</h1>
|
<h1 className="hero-display" id="cat-page-heading">{t("categories.heading")}</h1>
|
||||||
<p className="lead">
|
<p className="lead">
|
||||||
{total} standards across {categories.length} building material sections.
|
{t("categories.lead", { count: categories.length, total })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="tile tile-light" aria-label="All categories">
|
<section className="tile tile-light" aria-label={t("categories.allLabel")}>
|
||||||
<div className="tile-inner">
|
<div className="tile-inner">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="cat-skeleton">
|
<div className="cat-skeleton">
|
||||||
@@ -78,7 +80,7 @@ export default function Categories() {
|
|||||||
{CATEGORY_ICONS[cat.name] || "📋"}
|
{CATEGORY_ICONS[cat.name] || "📋"}
|
||||||
</span>
|
</span>
|
||||||
<span className="cat-page-name">{cat.name}</span>
|
<span className="cat-page-name">{cat.name}</span>
|
||||||
<span className="cat-page-count">{cat.count} standards</span>
|
<span className="cat-page-count">{t("categories.standardCount", { count: cat.count })}</span>
|
||||||
<span className="cat-page-arrow" aria-hidden="true">→</span>
|
<span className="cat-page-arrow" aria-hidden="true">→</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { fetchStats, fetchCategories } from "../api/standards";
|
import { fetchStats, fetchCategories } from "../api/standards";
|
||||||
import "./Home.css";
|
import "./Home.css";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@@ -20,19 +22,24 @@ export default function Home() {
|
|||||||
else navigate("/standards");
|
else navigate("/standards");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PILLARS = [
|
||||||
|
{ icon: "⚡", titleKey: "home.pillar_instantRetrieval_title", bodyKey: "home.pillar_instantRetrieval_body" },
|
||||||
|
{ icon: "📐", titleKey: "home.pillar_sectionDetail_title", bodyKey: "home.pillar_sectionDetail_body" },
|
||||||
|
{ icon: "🗂", titleKey: "home.pillar_categories_title", bodyKey: "home.pillar_categories_body" },
|
||||||
|
{ icon: "🔒", titleKey: "home.pillar_officialSource_title", bodyKey: "home.pillar_officialSource_body" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
{/* Hero */}
|
|
||||||
<section className="tile tile-dark hero-tile" aria-labelledby="hero-heading">
|
<section className="tile tile-dark hero-tile" aria-labelledby="hero-heading">
|
||||||
<div className="tile-inner tile-center">
|
<div className="tile-inner tile-center">
|
||||||
<p className="tile-eyebrow">Special Publication 21 · 2005</p>
|
<p className="tile-eyebrow">{t("home.eyebrow")}</p>
|
||||||
<h1 className="hero-display" id="hero-heading">
|
<h1 className="hero-display" id="hero-heading">
|
||||||
Handbook of<br />Building Materials
|
{t("home.heroTitle").split("\n").map((line, i, arr) => (
|
||||||
|
<span key={i}>{line}{i < arr.length - 1 && <br />}</span>
|
||||||
|
))}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="lead">
|
<p className="lead">{t("home.heroLead")}</p>
|
||||||
Indian Standards across 25 material categories —<br className="desktop-only" />
|
|
||||||
searchable, categorised, and ready to reference.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form className="hero-search-form" onSubmit={handleSearch} role="search">
|
<form className="hero-search-form" onSubmit={handleSearch} role="search">
|
||||||
<div className="hero-search-wrap">
|
<div className="hero-search-wrap">
|
||||||
@@ -42,40 +49,39 @@ export default function Home() {
|
|||||||
type="search"
|
type="search"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search standards, e.g. Portland Cement, IS 269…"
|
placeholder={t("home.searchPlaceholder")}
|
||||||
aria-label="Search standards"
|
aria-label={t("home.searchLabel")}
|
||||||
/>
|
/>
|
||||||
<button className="btn-primary hero-search-btn" type="submit">Search</button>
|
<button className="btn-primary hero-search-btn" type="submit">{t("home.searchBtn")}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
<div className="hero-stats" aria-label="Key statistics">
|
<div className="hero-stats" aria-label={t("home.statsLabel")}>
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<span className="stat-num">{stats.totalStandards}</span>
|
<span className="stat-num">{stats.totalStandards}</span>
|
||||||
<span className="stat-label">IS Standards</span>
|
<span className="stat-label">{t("home.statStandards")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-divider" aria-hidden="true" />
|
<div className="stat-divider" aria-hidden="true" />
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<span className="stat-num">{stats.totalCategories}</span>
|
<span className="stat-num">{stats.totalCategories}</span>
|
||||||
<span className="stat-label">Categories</span>
|
<span className="stat-label">{t("home.statCategories")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-divider" aria-hidden="true" />
|
<div className="stat-divider" aria-hidden="true" />
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<span className="stat-num">929</span>
|
<span className="stat-num">929</span>
|
||||||
<span className="stat-label">Pages Indexed</span>
|
<span className="stat-label">{t("home.statPages")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Categories */}
|
|
||||||
<section className="tile tile-parchment" id="categories" aria-labelledby="cat-heading">
|
<section className="tile tile-parchment" id="categories" aria-labelledby="cat-heading">
|
||||||
<div className="tile-inner">
|
<div className="tile-inner">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h2 className="display-lg" id="cat-heading">25 Material Categories</h2>
|
<h2 className="display-lg" id="cat-heading">{t("home.categoriesHeading")}</h2>
|
||||||
<p className="lead-sub">Every building material section from SP‑21, indexed and searchable.</p>
|
<p className="lead-sub">{t("home.categoriesLead")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="category-grid" role="list">
|
<div className="category-grid" role="list">
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
@@ -86,43 +92,35 @@ export default function Home() {
|
|||||||
onClick={() => navigate(`/standards?category=${encodeURIComponent(cat.name)}`)}
|
onClick={() => navigate(`/standards?category=${encodeURIComponent(cat.name)}`)}
|
||||||
>
|
>
|
||||||
<span className="cat-name">{cat.name}</span>
|
<span className="cat-name">{cat.name}</span>
|
||||||
<span className="cat-count">{cat.count} standard{cat.count !== 1 ? "s" : ""}</span>
|
<span className="cat-count">{t("home.standardCount", { count: cat.count })}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* About strip */}
|
<section className="tile tile-dark-2" aria-labelledby="about-strip-heading">
|
||||||
<section className="tile tile-dark-2" aria-labelledby="about-heading">
|
|
||||||
<div className="tile-inner">
|
<div className="tile-inner">
|
||||||
<div className="feature-cols">
|
<div className="feature-cols">
|
||||||
<div className="feature-text">
|
<div className="feature-text">
|
||||||
<p className="tile-eyebrow">About SP‑21</p>
|
<p className="tile-eyebrow">{t("home.aboutEyebrow")}</p>
|
||||||
<h2 className="display-md" id="about-heading">
|
<h2 className="display-md" id="about-strip-heading">{t("home.aboutHeading")}</h2>
|
||||||
India's Reference for Building Material Standards
|
<p className="body-copy">{t("home.aboutBody")}</p>
|
||||||
</h2>
|
|
||||||
<p className="body-copy">
|
|
||||||
BIS Special Publication 21 consolidates all Indian Standards relevant to building and
|
|
||||||
construction materials — from Portland cement to wire ropes, sanitary fittings to structural
|
|
||||||
steels. Published by the Bureau of Indian Standards, it is the authoritative handbook used
|
|
||||||
by architects, structural engineers, contractors, and quality inspectors across India.
|
|
||||||
</p>
|
|
||||||
<a
|
<a
|
||||||
className="btn-primary-on-dark"
|
className="btn-primary-on-dark"
|
||||||
href="https://www.bis.gov.in"
|
href="https://www.bis.gov.in"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Visit BIS Portal ↗
|
{t("home.visitBIS")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="feature-pillars" role="list">
|
<div className="feature-pillars" role="list">
|
||||||
{PILLARS.map(({ icon, title, body }) => (
|
{PILLARS.map(({ icon, titleKey, bodyKey }) => (
|
||||||
<div className="pillar" role="listitem" key={title}>
|
<div className="pillar" role="listitem" key={titleKey}>
|
||||||
<span className="pillar-icon" aria-hidden="true">{icon}</span>
|
<span className="pillar-icon" aria-hidden="true">{icon}</span>
|
||||||
<h3 className="pillar-title">{title}</h3>
|
<h3 className="pillar-title">{t(titleKey)}</h3>
|
||||||
<p className="pillar-body">{body}</p>
|
<p className="pillar-body">{t(bodyKey)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,13 +131,6 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PILLARS = [
|
|
||||||
{ icon: "⚡", title: "Instant Retrieval", body: "Full-text search across all 573 standards with ranked results." },
|
|
||||||
{ icon: "📐", title: "Section-Level Detail", body: "Scope, requirements, delivery conditions — all structured fields." },
|
|
||||||
{ icon: "🗂", title: "25 Categories", body: "Organised by BIS material sections, mirroring SP‑21's own structure." },
|
|
||||||
{ icon: "🔒", title: "Official Source", body: "Parsed directly from the BIS SP‑21 : 2005 authoritative edition." },
|
|
||||||
];
|
|
||||||
|
|
||||||
function SearchIcon() {
|
function SearchIcon() {
|
||||||
return (
|
return (
|
||||||
<svg className="search-icon" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
<svg className="search-icon" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { recommend } from "../api/standards";
|
import { recommend } from "../api/standards";
|
||||||
import StandardModal from "../components/StandardModal";
|
import StandardModal from "../components/StandardModal";
|
||||||
import "./Recommend.css";
|
import "./Recommend.css";
|
||||||
|
|
||||||
export default function Recommend() {
|
export default function Recommend() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [rewrite, setRewrite] = useState(false);
|
const [rewrite, setRewrite] = useState(false);
|
||||||
const [results, setResults] = useState(null);
|
const [results, setResults] = useState(null);
|
||||||
@@ -25,7 +27,7 @@ export default function Recommend() {
|
|||||||
const data = await recommend({ query: q, top_n: 5, rewrite });
|
const data = await recommend({ query: q, top_n: 5, rewrite });
|
||||||
setResults(data);
|
setResults(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || "Something went wrong. Is the server running?");
|
setError(err.message || t("common.serverError"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -40,22 +42,17 @@ export default function Recommend() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="recommend-page">
|
<main className="recommend-page">
|
||||||
{/* Header tile */}
|
|
||||||
<section className="tile tile-dark rec-hero" aria-labelledby="rec-heading">
|
<section className="tile tile-dark rec-hero" aria-labelledby="rec-heading">
|
||||||
<div className="tile-inner tile-center">
|
<div className="tile-inner tile-center">
|
||||||
<p className="tile-eyebrow">Hybrid Retrieval · AI Explanation</p>
|
<p className="tile-eyebrow">{t("recommend.eyebrow")}</p>
|
||||||
<h1 className="hero-display" id="rec-heading">Find & Understand Standards</h1>
|
<h1 className="hero-display" id="rec-heading">{t("recommend.heading")}</h1>
|
||||||
<p className="lead">
|
<p className="lead">{t("recommend.lead")}</p>
|
||||||
Ask a natural language question — the system retrieves the most relevant
|
|
||||||
IS standards using dense + sparse search, then explains each in plain English.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Search tile */}
|
<section className="tile tile-parchment" aria-label={t("recommend.eyebrow")}>
|
||||||
<section className="tile tile-parchment" aria-label="Recommendation search">
|
|
||||||
<div className="tile-inner">
|
<div className="tile-inner">
|
||||||
<form onSubmit={handleSubmit} role="search" aria-label="Recommend standards">
|
<form onSubmit={handleSubmit} role="search" aria-label={t("recommend.heading")}>
|
||||||
<div className="rec-search-wrap">
|
<div className="rec-search-wrap">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
<input
|
<input
|
||||||
@@ -64,8 +61,8 @@ export default function Recommend() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="e.g. What standard covers tensile strength of structural steel?"
|
placeholder={t("recommend.searchPlaceholder")}
|
||||||
aria-label="Search query"
|
aria-label={t("recommend.searchLabel")}
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
@@ -74,36 +71,35 @@ export default function Recommend() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="rec-clear"
|
className="rec-clear"
|
||||||
onClick={() => { setQuery(""); setResults(null); inputRef.current?.focus(); }}
|
onClick={() => { setQuery(""); setResults(null); inputRef.current?.focus(); }}
|
||||||
aria-label="Clear"
|
aria-label={t("recommend.clearBtn")}
|
||||||
>✕</button>
|
>✕</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rec-options-row">
|
<div className="rec-options-row">
|
||||||
<label className="rewrite-toggle" title="Let the AI rephrase your query into precise IS keywords before searching">
|
<label className="rewrite-toggle" title={t("recommend.rewriteHint")}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={rewrite}
|
checked={rewrite}
|
||||||
onChange={(e) => setRewrite(e.target.checked)}
|
onChange={(e) => setRewrite(e.target.checked)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<span>Smart query rewrite</span>
|
<span>{t("recommend.rewriteLabel")}</span>
|
||||||
<span className="rewrite-hint">AI refines your query before searching</span>
|
<span className="rewrite-hint">{t("recommend.rewriteHint")}</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
className="btn-primary rec-submit"
|
className="btn-primary rec-submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!query.trim() || loading}
|
disabled={!query.trim() || loading}
|
||||||
>
|
>
|
||||||
{loading ? <><SpinIcon /> Searching…</> : "Find Standards"}
|
{loading ? <><SpinIcon /> {t("recommend.submitting")}</> : t("recommend.submitBtn")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Example queries */}
|
|
||||||
{!results && !loading && (
|
{!results && !loading && (
|
||||||
<div className="example-queries" aria-label="Example queries">
|
<div className="example-queries" aria-label={t("recommend.exampleLabel")}>
|
||||||
<p className="example-label">Try an example:</p>
|
<p className="example-label">{t("recommend.exampleLabel")}</p>
|
||||||
<div className="example-chips">
|
<div className="example-chips">
|
||||||
{EXAMPLE_QUERIES.map((q) => (
|
{EXAMPLE_QUERIES.map((q) => (
|
||||||
<button
|
<button
|
||||||
@@ -120,21 +116,20 @@ export default function Recommend() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Results tile */}
|
|
||||||
{(loading || results || error) && (
|
{(loading || results || error) && (
|
||||||
<section className="tile tile-light results-section" aria-live="polite" aria-label="Results">
|
<section className="tile tile-light results-section" aria-live="polite" aria-label={t("recommend.heading")}>
|
||||||
<div className="tile-inner">
|
<div className="tile-inner">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error-banner" role="alert">
|
<div className="error-banner" role="alert">
|
||||||
<strong>Error:</strong> {error}
|
<strong>{t("recommend.error_prefix")}</strong> {error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="loading-state" aria-label="Loading results">
|
<div className="loading-state" aria-label={t("common.loading")}>
|
||||||
<div className="loading-steps">
|
<div className="loading-steps">
|
||||||
<LoadingStep icon="🔍" label="Running hybrid retrieval (FAISS + BM25)…" />
|
<LoadingStep icon="🔍" label={t("recommend.loadingRetrieval")} />
|
||||||
<LoadingStep icon="✦" label="Generating AI explanations…" delay />
|
<LoadingStep icon="✦" label={t("recommend.loadingAI")} delay />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -144,14 +139,14 @@ export default function Recommend() {
|
|||||||
<div className="results-header">
|
<div className="results-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="results-title">
|
<h2 className="results-title">
|
||||||
{results.standards.length} Standard{results.standards.length !== 1 ? "s" : ""} Found
|
{t("recommend.resultsFound", { count: results.standards.length })}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="results-query">for: <em>{results.query}</em></p>
|
<p className="results-query">{t("recommend.resultsFor")} <em>{results.query}</em></p>
|
||||||
</div>
|
</div>
|
||||||
<div className="latency-badge" aria-label="Timing breakdown">
|
<div className="latency-badge" aria-label={t("recommend.timingLabel")}>
|
||||||
<LatencyBadge label="Retrieval" ms={results.latency.retrieval_ms} />
|
<LatencyBadge label={t("recommend.retrieval")} ms={results.latency.retrieval_ms} />
|
||||||
<LatencyBadge label="AI" ms={results.latency.llm_ms} accent />
|
<LatencyBadge label={t("recommend.ai")} ms={results.latency.llm_ms} accent />
|
||||||
<LatencyBadge label="Total" ms={results.latency.total_ms} bold />
|
<LatencyBadge label={t("recommend.total")} ms={results.latency.total_ms} bold />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,6 +157,7 @@ export default function Recommend() {
|
|||||||
standard={s}
|
standard={s}
|
||||||
rank={i + 1}
|
rank={i + 1}
|
||||||
onOpen={() => setSelected(standardsFullRecord(s))}
|
onOpen={() => setSelected(standardsFullRecord(s))}
|
||||||
|
t={t}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -178,9 +174,7 @@ export default function Recommend() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sub-components ──────────────────────────────────────────────────────────
|
function RecommendCard({ standard, rank, onOpen, t }) {
|
||||||
|
|
||||||
function RecommendCard({ standard, rank, onOpen }) {
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className="rec-card"
|
className="rec-card"
|
||||||
@@ -188,7 +182,7 @@ function RecommendCard({ standard, rank, onOpen }) {
|
|||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
onKeyDown={(e) => e.key === "Enter" && onOpen()}
|
onKeyDown={(e) => e.key === "Enter" && onOpen()}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`Rank ${rank}: ${standard.standard_id}`}
|
aria-label={t("recommend.rankLabel", { rank, id: standard.standard_id })}
|
||||||
>
|
>
|
||||||
<div className="rec-card-rank" aria-hidden="true">{rank}</div>
|
<div className="rec-card-rank" aria-hidden="true">{rank}</div>
|
||||||
|
|
||||||
@@ -197,21 +191,21 @@ function RecommendCard({ standard, rank, onOpen }) {
|
|||||||
<span className="card-cat">{standard.category}</span>
|
<span className="card-cat">{standard.category}</span>
|
||||||
<span className="card-id">{standard.standard_id}</span>
|
<span className="card-id">{standard.standard_id}</span>
|
||||||
{standard.matched_section && (
|
{standard.matched_section && (
|
||||||
<span className="rec-card-section">§ {standard.matched_section}</span>
|
<span className="rec-card-section">{t("recommend.section", { section: standard.matched_section })}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="rec-card-title">{standard.title}</h3>
|
<h3 className="rec-card-title">{standard.title}</h3>
|
||||||
|
|
||||||
{standard.explanation && (
|
{standard.explanation && (
|
||||||
<div className="rec-card-explanation" aria-label="AI explanation">
|
<div className="rec-card-explanation" aria-label={t("recommend.aiExplanation")}>
|
||||||
<span className="explanation-icon" aria-hidden="true">✦</span>
|
<span className="explanation-icon" aria-hidden="true">✦</span>
|
||||||
<p className="explanation-text">{standard.explanation}</p>
|
<p className="explanation-text">{standard.explanation}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{standard.keywords?.length > 0 && (
|
{standard.keywords?.length > 0 && (
|
||||||
<div className="card-keywords" aria-label="Keywords">
|
<div className="card-keywords" aria-label={t("recommend.keywords")}>
|
||||||
{standard.keywords.slice(0, 5).map((kw) => (
|
{standard.keywords.slice(0, 5).map((kw) => (
|
||||||
<span className="keyword-chip" key={kw}>{kw}</span>
|
<span className="keyword-chip" key={kw}>{kw}</span>
|
||||||
))}
|
))}
|
||||||
@@ -219,7 +213,7 @@ function RecommendCard({ standard, rank, onOpen }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rec-card-score" aria-label={`Relevance score ${standard.score}`}>
|
<div className="rec-card-score" aria-label={t("recommend.relevanceScore", { score: standard.score })}>
|
||||||
<span className="score-num">{(standard.score * 100).toFixed(0)}</span>
|
<span className="score-num">{(standard.score * 100).toFixed(0)}</span>
|
||||||
<span className="score-label">score</span>
|
<span className="score-label">score</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,7 +254,6 @@ function SpinIcon() {
|
|||||||
return <span className="spin-icon" aria-hidden="true">⟳</span>;
|
return <span className="spin-icon" aria-hidden="true">⟳</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge recommendation result with full standard record for the modal
|
|
||||||
function standardsFullRecord(s) {
|
function standardsFullRecord(s) {
|
||||||
return {
|
return {
|
||||||
standard_id: s.standard_id,
|
standard_id: s.standard_id,
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { fetchStandards, fetchCategories } from "../api/standards";
|
import { fetchStandards, fetchCategories } from "../api/standards";
|
||||||
import StandardCard from "../components/StandardCard";
|
import StandardCard from "../components/StandardCard";
|
||||||
import StandardModal from "../components/StandardModal";
|
import StandardModal from "../components/StandardModal";
|
||||||
|
import { useDebounce } from "../hooks/useDebounce";
|
||||||
import "./Standards.css";
|
import "./Standards.css";
|
||||||
|
|
||||||
const PAGE_SIZE = 18;
|
const PAGE_SIZE = 18;
|
||||||
|
|
||||||
/**
|
|
||||||
* Standards search page with pagination.
|
|
||||||
* Uses URL search params for query/category state.
|
|
||||||
* Loads standards on mount and handles user interactions.
|
|
||||||
*/
|
|
||||||
export default function Standards() {
|
export default function Standards() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [query, setQuery] = useState(searchParams.get("q") || "");
|
const [query, setQuery] = useState(searchParams.get("q") || "");
|
||||||
|
const debouncedQuery = useDebounce(query, 350);
|
||||||
const [category, setCategory] = useState(searchParams.get("category") || "");
|
const [category, setCategory] = useState(searchParams.get("category") || "");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
@@ -35,32 +34,37 @@ export default function Standards() {
|
|||||||
setMeta(data.meta);
|
setMeta(data.meta);
|
||||||
setPage(pg);
|
setPage(pg);
|
||||||
} catch {
|
} catch {
|
||||||
setError("Could not load standards. Is the server running?");
|
setError(t("standards.serverError"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories().then(setCategories).catch(() => {});
|
fetchCategories().then(setCategories).catch(() => {});
|
||||||
load(query, category, 1);
|
load(debouncedQuery, category, 1);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(debouncedQuery, category, 1);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [debouncedQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = {};
|
const params = {};
|
||||||
if (query) params.q = query;
|
if (debouncedQuery) params.q = debouncedQuery;
|
||||||
if (category) params.category = category;
|
if (category) params.category = category;
|
||||||
setSearchParams(params, { replace: true });
|
setSearchParams(params, { replace: true });
|
||||||
}, [query, category, setSearchParams]);
|
}, [debouncedQuery, category, setSearchParams]);
|
||||||
|
|
||||||
const handleCategoryChange = (value) => {
|
const handleCategoryChange = (value) => {
|
||||||
setCategory(value);
|
setCategory(value);
|
||||||
load(query, value, 1);
|
load(debouncedQuery, value, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (pg) => {
|
const handlePageChange = (pg) => {
|
||||||
load(query, category, pg);
|
load(debouncedQuery, category, pg);
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,8 +78,8 @@ export default function Standards() {
|
|||||||
<main className="standards-page">
|
<main className="standards-page">
|
||||||
<section className="tile tile-parchment search-tile" aria-labelledby="search-heading">
|
<section className="tile tile-parchment search-tile" aria-labelledby="search-heading">
|
||||||
<div className="tile-inner">
|
<div className="tile-inner">
|
||||||
<h1 className="display-lg" id="search-heading">Find an IS Standard</h1>
|
<h1 className="display-lg" id="search-heading">{t("standards.heading")}</h1>
|
||||||
<p className="lead-sub">Search by standard number, title, material, or keyword.</p>
|
<p className="lead-sub">{t("standards.lead")}</p>
|
||||||
|
|
||||||
<form className="search-form" role="search" onSubmit={(e) => e.preventDefault()}>
|
<form className="search-form" role="search" onSubmit={(e) => e.preventDefault()}>
|
||||||
<div className="search-wrap">
|
<div className="search-wrap">
|
||||||
@@ -85,15 +89,15 @@ export default function Standards() {
|
|||||||
type="search"
|
type="search"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="e.g. Ordinary Portland Cement, IS 269, aggregates…"
|
placeholder={t("standards.searchPlaceholder")}
|
||||||
aria-label="Search standards"
|
aria-label={t("standards.searchLabel")}
|
||||||
/>
|
/>
|
||||||
{query && (
|
{query && (
|
||||||
<button
|
<button
|
||||||
className="search-clear"
|
className="search-clear"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearSearch}
|
onClick={handleClearSearch}
|
||||||
aria-label="Clear search"
|
aria-label={t("standards.clearSearch")}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@@ -104,9 +108,9 @@ export default function Standards() {
|
|||||||
className="category-filter"
|
className="category-filter"
|
||||||
value={category}
|
value={category}
|
||||||
onChange={(e) => handleCategoryChange(e.target.value)}
|
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||||
aria-label="Filter by category"
|
aria-label={t("standards.categoryFilter")}
|
||||||
>
|
>
|
||||||
<option value="">All Categories</option>
|
<option value="">{t("standards.allCategories")}</option>
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<option key={c.name} value={c.name}>
|
<option key={c.name} value={c.name}>
|
||||||
{c.name} ({c.count})
|
{c.name} ({c.count})
|
||||||
@@ -124,8 +128,10 @@ export default function Standards() {
|
|||||||
|
|
||||||
{!error && meta && (
|
{!error && meta && (
|
||||||
<p className="results-meta">
|
<p className="results-meta">
|
||||||
{loading ? "Searching…" : `${meta.total} standard${meta.total !== 1 ? "s" : ""} found`}
|
{loading
|
||||||
{meta.total > 0 && ` — page ${meta.page} of ${meta.totalPages}`}
|
? t("standards.searching")
|
||||||
|
: t("standards.found", { count: meta.total })}
|
||||||
|
{!loading && meta.total > 0 && ` — ${t("standards.page", { page: meta.page, total: meta.totalPages })}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -139,8 +145,8 @@ export default function Standards() {
|
|||||||
|
|
||||||
{!loading && results.length === 0 && !error && (
|
{!loading && results.length === 0 && !error && (
|
||||||
<div className="results-empty">
|
<div className="results-empty">
|
||||||
<p className="empty-title">No standards found</p>
|
<p className="empty-title">{t("standards.noResultsTitle")}</p>
|
||||||
<p className="empty-sub">Try a different keyword or clear the category filter.</p>
|
<p className="empty-sub">{t("standards.noResultsSub")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -157,14 +163,14 @@ export default function Standards() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{meta && meta.totalPages > 1 && (
|
{meta && meta.totalPages > 1 && (
|
||||||
<nav className="pagination" aria-label="Results pagination">
|
<nav className="pagination" aria-label={t("standards.pagination")}>
|
||||||
<button
|
<button
|
||||||
className="page-btn"
|
className="page-btn"
|
||||||
disabled={page <= 1}
|
disabled={page <= 1}
|
||||||
onClick={() => handlePageChange(page - 1)}
|
onClick={() => handlePageChange(page - 1)}
|
||||||
aria-label="Previous page"
|
aria-label={t("standards.prevPage")}
|
||||||
>
|
>
|
||||||
← Prev
|
← {t("standards.prevPage")}
|
||||||
</button>
|
</button>
|
||||||
<div className="page-numbers">
|
<div className="page-numbers">
|
||||||
{buildPageRange(page, meta.totalPages).map((p, i) =>
|
{buildPageRange(page, meta.totalPages).map((p, i) =>
|
||||||
@@ -175,7 +181,7 @@ export default function Standards() {
|
|||||||
key={p}
|
key={p}
|
||||||
className={`page-btn${p === page ? " active" : ""}`}
|
className={`page-btn${p === page ? " active" : ""}`}
|
||||||
onClick={() => handlePageChange(p)}
|
onClick={() => handlePageChange(p)}
|
||||||
aria-label={`Page ${p}`}
|
aria-label={t("standards.pageLabel", { page: p })}
|
||||||
aria-current={p === page ? "page" : undefined}
|
aria-current={p === page ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{p}
|
{p}
|
||||||
@@ -187,9 +193,9 @@ export default function Standards() {
|
|||||||
className="page-btn"
|
className="page-btn"
|
||||||
disabled={page >= meta.totalPages}
|
disabled={page >= meta.totalPages}
|
||||||
onClick={() => handlePageChange(page + 1)}
|
onClick={() => handlePageChange(page + 1)}
|
||||||
aria-label="Next page"
|
aria-label={t("standards.nextPage")}
|
||||||
>
|
>
|
||||||
Next →
|
{t("standards.nextPage")} →
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user