Initial commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { Send, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
const initialState = {
|
||||
name: "",
|
||||
email: "",
|
||||
mobile: "",
|
||||
companyName: "",
|
||||
designation: "",
|
||||
message: ""
|
||||
};
|
||||
export default function ContactForm() {
|
||||
const [form, setForm] = useState(initialState);
|
||||
const [status, setStatus] = useState("idle");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setForm((f) => ({ ...f, [name]: value }));
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setStatus("sending");
|
||||
setErrorMsg("");
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
setStatus("sent");
|
||||
setForm(initialState);
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setErrorMsg("Something went wrong. Please try again.");
|
||||
}
|
||||
};
|
||||
return <div className="relative w-full">
|
||||
<AnimatePresence mode="wait">
|
||||
{status === "sent" ? <motion.div
|
||||
key="success"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="p-8 md:p-12 flex flex-col items-center text-center bg-white border border-slate-100 rounded-2xl shadow-xl"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-full bg-emerald-50 flex items-center justify-center mb-5 border border-emerald-100">
|
||||
<CheckCircle2 className="w-7 h-7 text-emerald-600" />
|
||||
</div>
|
||||
<h3 className="font-heading text-xl text-slate-900 font-extrabold tracking-tight">Message Received</h3>
|
||||
<p className="text-slate-500 mt-3 max-w-md font-light leading-relaxed text-sm">
|
||||
Thank you for reaching out to Masar NDT. Our expert engineering team will review your enquiry and get back to you within 24 hours.
|
||||
</p>
|
||||
<button
|
||||
className="mt-8 text-xs text-white bg-orange-500 hover:bg-orange-600 px-8 py-3.5 font-bold tracking-wider uppercase rounded-xl transition-all shadow-md hover:shadow-lg hover:-translate-y-0.5 duration-200"
|
||||
onClick={() => setStatus("idle")}
|
||||
>
|
||||
Send another message
|
||||
</button>
|
||||
</motion.div> : <motion.form
|
||||
key="form"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
onSubmit={handleSubmit}
|
||||
className="p-6 md:p-8 space-y-6 bg-white border border-slate-100 shadow-xl rounded-2xl"
|
||||
>
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
<Field label="Your Name" name="name" value={form.name} onChange={handleChange} required />
|
||||
<Field label="Email Address" name="email" type="email" value={form.email} onChange={handleChange} required />
|
||||
<Field label="Mobile Number" name="mobile" type="tel" value={form.mobile} onChange={handleChange} />
|
||||
<Field label="Company Name" name="companyName" value={form.companyName} onChange={handleChange} />
|
||||
</div>
|
||||
<Field label="Designation / Job Title" name="designation" value={form.designation} onChange={handleChange} />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="message" className="text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-2 font-heading">
|
||||
Message / Scope Details *
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows={4}
|
||||
value={form.message}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full bg-slate-50/50 border border-slate-200 hover:border-orange-500/30 rounded-xl px-4 py-3 text-sm text-slate-900 focus:border-orange-500 focus:bg-white focus:ring-2 focus:ring-orange-500/10 outline-none transition-all resize-y font-normal leading-relaxed"
|
||||
placeholder="Describe your NDT, inspection, or technical manpower requirements..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === "error" && <div className="flex items-center gap-2.5 text-sm text-red-600 bg-red-50 border border-red-100 p-4 rounded-xl">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
<p>{errorMsg}</p>
|
||||
</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "sending"}
|
||||
className="w-full sm:w-auto flex items-center justify-center gap-2 bg-orange-500 hover:bg-orange-600 text-white font-bold text-xs tracking-wider uppercase px-8 py-4 rounded-xl transition-all duration-200 disabled:opacity-60 shadow-lg hover:shadow-orange-500/10 hover:-translate-y-0.5 active:translate-y-0"
|
||||
>
|
||||
{status === "sending" ? <>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
<span>Submitting…</span>
|
||||
</> : <>
|
||||
<span>Submit Enquiry</span>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
</>}
|
||||
</button>
|
||||
</motion.form>}
|
||||
</AnimatePresence>
|
||||
</div>;
|
||||
}
|
||||
function Field({ label, name, value, onChange, type = "text", required = false }) {
|
||||
return <div className="flex flex-col">
|
||||
<label htmlFor={name} className="text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-2 font-heading">
|
||||
{label} {required && <span className="text-orange-500">*</span>}
|
||||
</label>
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
required={required}
|
||||
className="w-full bg-slate-50/50 border border-slate-200 hover:border-orange-500/30 rounded-xl px-4 py-3 text-sm text-slate-900 focus:border-orange-500 focus:bg-white focus:ring-2 focus:ring-orange-500/10 outline-none transition-all font-normal"
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { serviceCategories, offices, company } from "../data/content";
|
||||
import { Mail, Phone, MapPin, Clock } from "lucide-react";
|
||||
export default function Footer() {
|
||||
return <footer className="bg-slate-950 text-slate-200 mt-24 border-t border-slate-900">
|
||||
{
|
||||
/* Upper Grid Area */
|
||||
}
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8 py-16 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-12">
|
||||
|
||||
{
|
||||
/* Col 1: Overview */
|
||||
}
|
||||
<div className="space-y-5">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<span className="w-4 h-4 rounded-sm bg-orange-500 shadow-md" />
|
||||
<span className="font-heading font-extrabold uppercase tracking-wider text-white text-lg">
|
||||
Masar<span className="text-orange-500"> NDT</span>
|
||||
</span>
|
||||
</Link>
|
||||
<p className="text-xs leading-relaxed text-slate-400 font-light">
|
||||
{company.intro}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-wider font-heading">
|
||||
A member of {company.legalName}
|
||||
</p>
|
||||
<div className="flex items-center gap-3.5 pt-2">
|
||||
<a href="#" className="p-2.5 bg-slate-900 hover:bg-orange-500 hover:text-white transition-all rounded-lg border border-slate-800" aria-label="LinkedIn">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.779-1.75-1.75s.784-1.75 1.75-1.75 1.75.779 1.75 1.75-.784 1.75-1.75 1.75zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="p-2.5 bg-slate-900 hover:bg-orange-500 hover:text-white transition-all rounded-lg border border-slate-800" aria-label="Facebook">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Col 2: Services */
|
||||
}
|
||||
<div>
|
||||
<h3 className="font-heading text-xs font-bold text-white uppercase tracking-wider mb-5 border-l-2 border-orange-500 pl-3">
|
||||
Services
|
||||
</h3>
|
||||
<ul className="space-y-3 text-xs text-slate-400">
|
||||
{serviceCategories.map((s) => <li key={s.id}>
|
||||
<Link to={s.path} className="hover:text-orange-500 transition-colors duration-200 block py-0.5 font-medium">
|
||||
{s.title}
|
||||
</Link>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Col 3: Quick Links */
|
||||
}
|
||||
<div>
|
||||
<h3 className="font-heading text-xs font-bold text-white uppercase tracking-wider mb-5 border-l-2 border-orange-500 pl-3">
|
||||
Quick Links
|
||||
</h3>
|
||||
<ul className="space-y-3 text-xs text-slate-400">
|
||||
<li><Link to="/about" className="hover:text-orange-500 transition-colors block py-0.5 font-medium">About Us</Link></li>
|
||||
<li><Link to="/industries" className="hover:text-orange-500 transition-colors block py-0.5 font-medium">Industries Served</Link></li>
|
||||
<li><Link to="/projects" className="hover:text-orange-500 transition-colors block py-0.5 font-medium">Project Showcase</Link></li>
|
||||
<li><Link to="/gallery" className="hover:text-orange-500 transition-colors block py-0.5 font-medium">Media Gallery</Link></li>
|
||||
<li><Link to="/contact" className="hover:text-orange-500 transition-colors block py-0.5 font-medium">Get in Touch</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Col 4: Contact & Hours */
|
||||
}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="font-heading text-xs font-bold text-white uppercase tracking-wider mb-5 border-l-2 border-orange-500 pl-3">
|
||||
Contact Info
|
||||
</h3>
|
||||
<div className="space-y-3.5 text-xs text-slate-400">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<MapPin className="w-4 h-4 text-orange-500 shrink-0 mt-0.5" />
|
||||
<span className="leading-relaxed font-normal">{offices[0].lines.join(", ")}</span>
|
||||
</div>
|
||||
<a href="tel:+966571033252" className="flex items-center gap-2.5 hover:text-orange-500 transition-colors font-semibold">
|
||||
<Phone className="w-4 h-4 text-orange-500 shrink-0" />
|
||||
<span>+966 57 103 3252</span>
|
||||
</a>
|
||||
<a href="mailto:sales@masarndt.com" className="flex items-center gap-2.5 hover:text-orange-500 transition-colors font-medium">
|
||||
<Mail className="w-4 h-4 text-orange-500 shrink-0" />
|
||||
<span className="break-all text-orange-500 font-semibold">{offices[0].email}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-heading text-xs font-bold text-white uppercase tracking-wider mb-3 border-l-2 border-orange-500 pl-3">
|
||||
Business Hours
|
||||
</h3>
|
||||
<div className="flex items-center gap-2.5 text-xs text-slate-400 font-light">
|
||||
<Clock className="w-4 h-4 text-orange-500 shrink-0" />
|
||||
<span className="font-medium">Sun - Thu: 8:00 AM - 5:00 PM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Bottom Copyright Area */
|
||||
}
|
||||
<div className="border-t border-slate-900 bg-slate-950 py-8 text-xs text-slate-500">
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8 flex flex-col sm:flex-row justify-between items-center gap-4 text-center sm:text-left">
|
||||
<span className="font-light">
|
||||
© {(/* @__PURE__ */ new Date()).getFullYear()} Masar NDT. All rights reserved. Managed by AKMEC Group.
|
||||
</span>
|
||||
<div className="flex flex-wrap justify-center gap-4 font-bold text-[10px] uppercase tracking-wider">
|
||||
<span className="text-orange-500">ISO 9001:2015 Approved</span>
|
||||
<span className="text-slate-800">|</span>
|
||||
<Link to="/contact" className="hover:text-orange-400 transition-colors">Privacy Policy</Link>
|
||||
<span className="text-slate-800">|</span>
|
||||
<Link to="/contact" className="hover:text-orange-400 transition-colors">Terms of Use</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>;
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { ShieldCheck, Cpu, Database, Gauge, RefreshCw, Zap, CheckCircle } from "lucide-react";
|
||||
const assets = [
|
||||
{ id: "tube", name: "Heat Exchanger Tubes", icon: Cpu, description: "Tubular bundles in condensers, boilers, or evaporators." },
|
||||
{ id: "vessel", name: "Pressure Vessels & Shells", icon: Database, description: "Process drums, reactors, and high-pressure tanks." },
|
||||
{ id: "pipe", name: "Process & Utility Piping", icon: Gauge, description: "Cross-country pipelines, dead-legs, and small-bore systems." },
|
||||
{ id: "tank", name: "Storage Tank Bottoms", icon: ShieldCheck, description: "Crude and product storage tanks with potential ground-side corrosion." }
|
||||
];
|
||||
const concerns = [
|
||||
{ id: "thinning", name: "Wall Thinning & Erosion", description: "Internal or external metal loss from corrosive flow." },
|
||||
{ id: "cracks", name: "Surface & Welds Micro-cracks", description: "Fatigue cracking or thermal stress in weld joints." },
|
||||
{ id: "coating", name: "Coating Holiday / Voids", description: "Microscopic pinholes in protective epoxy layers." },
|
||||
{ id: "alloy", name: "Alloy Composition Mix-up", description: "Incorrect filler metal or grade installation." }
|
||||
];
|
||||
const getRecommendation = (assetId, concernId) => {
|
||||
if (assetId === "tube") {
|
||||
if (concernId === "thinning") {
|
||||
return {
|
||||
technique: "Remote Field Eddy Current Testing (RFET) & NFT",
|
||||
techCode: "RFET / NFT / IRIS",
|
||||
standards: ["ASME Section V Article 8", "ASTM E2096", "API 570"],
|
||||
prepRequired: ["Hydro-jetting cleaning (280\u2013560 kg/cm\xB2)", "Dummy probe sizing check", "Drying of tubes"],
|
||||
advantage: "Highly sensitive to wall loss in ferromagnetic boiler and air-cooler tubes.",
|
||||
simulatedSignalName: "Impedance Phase Shift Anomaly",
|
||||
simulatedSignalDesc: "Indicates localized pitting near support baffle.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "cracks") {
|
||||
return {
|
||||
technique: "Eddy Current Testing (ECT)",
|
||||
techCode: "ECT Tube Inspection",
|
||||
standards: ["ASME Section V Article 8", "ASTM E309", "API 510"],
|
||||
prepRequired: ["Internal brushing", "Calibration reference standard match", "Removal of scale"],
|
||||
advantage: "Extremely fast (up to 2m/s) detection of fine longitudinal cracks in non-ferrous tubing.",
|
||||
simulatedSignalName: "Lissajous Loop Phase Deflection",
|
||||
simulatedSignalDesc: "Reveals shallow inner-diameter surface breaking cracks.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "coating") {
|
||||
return {
|
||||
technique: "Specialized Internal Visual & Boroscopic Scan",
|
||||
techCode: "VT-Boroscopy",
|
||||
standards: ["ASME Section V Article 9", "API 510"],
|
||||
prepRequired: ["Total de-watering", "Dual-axis high-definition probe calibration"],
|
||||
advantage: "Visual verification of surface scaling and protective lining discoloration.",
|
||||
simulatedSignalName: "Optical Surface Mapping Anomaly",
|
||||
simulatedSignalDesc: "Visible pinhole lining breach with iron-sulfide deposits.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
}
|
||||
return {
|
||||
technique: "Positive Material Identification (PMI Spectroscopy)",
|
||||
techCode: "XRF / LIBS PMI",
|
||||
standards: ["API RP 578", "ASME Section II"],
|
||||
prepRequired: ["Oxide film sanding", "Surface temperature stability"],
|
||||
advantage: "Direct chemical verification of tube-to-tubesheet alloy grades.",
|
||||
simulatedSignalName: "X-Ray Emission Spectrum Peak",
|
||||
simulatedSignalDesc: "Identified trace elements conforming to Monel 400 specification.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
}
|
||||
if (assetId === "vessel") {
|
||||
if (concernId === "thinning") {
|
||||
return {
|
||||
technique: "Ultrasonic Corrosion Mapping & Thickness Testing",
|
||||
techCode: "UT-Corrosion Mapping",
|
||||
standards: ["ASME Section V Article 5", "API 510", "API 579-1 FFS"],
|
||||
prepRequired: ["Removal of loose rust/peeling paint", "High-temp couplant application", "Grid marking"],
|
||||
advantage: "Provides absolute remaining wall thickness calculations for Fitness-For-Service.",
|
||||
simulatedSignalName: "A-Scan Signal Time-of-Flight Shift",
|
||||
simulatedSignalDesc: "Significant shift in back-wall echo indicates localized wear loop.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "cracks") {
|
||||
return {
|
||||
technique: "Wet Fluorescent Magnetic Particle Testing (WFMT) or PAUT",
|
||||
techCode: "WFMT / Phased Array UT",
|
||||
standards: ["ASME Section V Article 4 & 7", "ASTM E1444", "API 510"],
|
||||
prepRequired: ["Grease degreasing", "UV black-light assembly setup", "Yoke calibration check"],
|
||||
advantage: "Detects extreme fine environmental stress corrosion cracks (SCC) in vessel welds.",
|
||||
simulatedSignalName: "Linear Magnetic Particle Indication",
|
||||
simulatedSignalDesc: "Fine transverse weld toe crack highlighted under UV spectrum.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "coating") {
|
||||
return {
|
||||
technique: "High-Voltage Holiday / Spark Testing",
|
||||
techCode: "HV Holiday Detection",
|
||||
standards: ["NACE SP0188", "ASTM D5162"],
|
||||
prepRequired: ["Lining curing verification", "Ground wire connection to bare steel", "Dry surface state"],
|
||||
advantage: "Uncovers invisible microscopic voids in internal rubber/glass-flake linings.",
|
||||
simulatedSignalName: "Audible Continuous Spark Arc",
|
||||
simulatedSignalDesc: "Voltage drop at 15kV identifies lining puncture.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
}
|
||||
return {
|
||||
technique: "Portable PMI Spectroscopy",
|
||||
techCode: "PMI Verification",
|
||||
standards: ["API RP 578", "ASME Section II"],
|
||||
prepRequired: ["Bare metal contact grinding", "Standards reference calibration"],
|
||||
advantage: "Ensures replacement shell plate or weld filler matches material datasheet specifications.",
|
||||
simulatedSignalName: "Chromium-Nickel Spectral Signal",
|
||||
simulatedSignalDesc: "Conforms to SS316L; matches code verification guidelines.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
}
|
||||
if (assetId === "pipe") {
|
||||
if (concernId === "thinning") {
|
||||
return {
|
||||
technique: "Manual & Automated Ultrasonic Thickness Testing (UT-T)",
|
||||
techCode: "UT Wall-Thickness",
|
||||
standards: ["ASME Section V Article 5", "API 570", "API 574"],
|
||||
prepRequired: ["Couplant brush-on", "Scale scraping of test point", "Zero-block calibration"],
|
||||
advantage: "Fast, precise remaining-life estimations in designated corrosion monitoring locations (CML).",
|
||||
simulatedSignalName: "UT Echo Loss",
|
||||
simulatedSignalDesc: "Reduced wall-thickness detected inside dead-leg bend.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "cracks") {
|
||||
return {
|
||||
technique: "Ultrasonic Shear-Wave / PAUT Inspections",
|
||||
techCode: "PAUT Weld Inspection",
|
||||
standards: ["ASME Section V Article 4", "API 570", "AWS D1.1"],
|
||||
prepRequired: ["Weld crown preparation", "Couplant liquid application", "Wedge scan path design"],
|
||||
advantage: "Volumetric weld-integrity scan; maps complete flaw depth, length, and slope.",
|
||||
simulatedSignalName: "B-Scan Cross-Section Reflection",
|
||||
simulatedSignalDesc: "Lack of root fusion anomaly identified at the 4-inch weld seam.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "coating") {
|
||||
return {
|
||||
technique: "Low-Voltage Sponge / Wet Sponge Holiday Testing",
|
||||
techCode: "LV Holiday Testing",
|
||||
standards: ["ASTM G62", "NACE SP0188"],
|
||||
prepRequired: ["Tap water wetting with surfactant", "Lining surface drying"],
|
||||
advantage: "Non-destructive testing for delicate pipeline paint or thin epoxy liners.",
|
||||
simulatedSignalName: "Audio Signal Indicator Trigger",
|
||||
simulatedSignalDesc: "Resistance drop reveals pinhole breach on the external pipe wrap.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
}
|
||||
return {
|
||||
technique: "Alloy Verification & Weld PMI Check",
|
||||
techCode: "XRF Alloy Analyzer",
|
||||
standards: ["API RP 578", "ASME Section IX"],
|
||||
prepRequired: ["Sanding of paint down to bare steel", "Analyzer sensor clean"],
|
||||
advantage: "Rapid verification of pipe fitting schedules and flange material compatibility.",
|
||||
simulatedSignalName: "Spectral Peak Verification",
|
||||
simulatedSignalDesc: "Conforms to ASTM A106 Grade B carbon steel specification.",
|
||||
dangerLevel: "Low"
|
||||
};
|
||||
}
|
||||
if (concernId === "thinning") {
|
||||
return {
|
||||
technique: "Magnetic Flux Leakage (MFL) Tank Bottom Floor Scanner",
|
||||
techCode: "MFL Scanning",
|
||||
standards: ["API 653 Annex C", "ASME Section V Article 16"],
|
||||
prepRequired: ["Comprehensive tank cleaning & sludge removal", "Floor drying", "Removal of heavy scale"],
|
||||
advantage: "Rapidly scans 100% of tank bottom plates for soil-side or product-side pitting.",
|
||||
simulatedSignalName: "MFL Flux Leakage Peak",
|
||||
simulatedSignalDesc: "High flux escape indicates ~45% bottom plate loss from the bottom soil side.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "cracks") {
|
||||
return {
|
||||
technique: "Vacuum Box Testing & Dye Penetrant (PT)",
|
||||
techCode: "Vacuum Box & PT Weld",
|
||||
standards: ["ASME Section V Article 10", "API 653"],
|
||||
prepRequired: ["Weld slag wire-brushing", "Liquid soap-bubble detergent prep", "Sufficient lighting"],
|
||||
advantage: "Provides 100% immediate bubble verification of potential annular plate weld seam leaks.",
|
||||
simulatedSignalName: "Detergent Bubble Indication",
|
||||
simulatedSignalDesc: "Active bubble train stream points to a fine through-wall pinhole weld leak.",
|
||||
dangerLevel: "High"
|
||||
};
|
||||
}
|
||||
if (concernId === "coating") {
|
||||
return {
|
||||
technique: "High-Voltage Pulse Holiday Testing",
|
||||
techCode: "Pulse Holiday NDT",
|
||||
standards: ["NACE SP0188", "API 652"],
|
||||
prepRequired: ["Coating curing check", "Continuous wire loop ground installation", "Dust wipe down"],
|
||||
advantage: "Safely checks heavy, multi-layer reinforced tank fiber lining systems.",
|
||||
simulatedSignalName: "High-Voltage Arc Spark",
|
||||
simulatedSignalDesc: "Discharge arc points to holiday location in sump lining weld.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
}
|
||||
return {
|
||||
technique: "XRF Alloy Analysis & Hardness Audit",
|
||||
techCode: "PMI Hardness Testing",
|
||||
standards: ["API 653", "ASTM E110"],
|
||||
prepRequired: ["Sanding weld crown flat", "Surface cleaning"],
|
||||
advantage: "Validates mechanical strength properties of heavy structural shell plate welds.",
|
||||
simulatedSignalName: "Brinell Hardness Value Return",
|
||||
simulatedSignalDesc: "185 HBW reading confirms material toughness falls in safe operating zone.",
|
||||
dangerLevel: "Medium"
|
||||
};
|
||||
};
|
||||
export default function InteractiveNdtAdvisor() {
|
||||
const [selectedAsset, setSelectedAsset] = useState("tube");
|
||||
const [selectedConcern, setSelectedConcern] = useState("thinning");
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [scanProgress, setScanProgress] = useState(0);
|
||||
const [showResult, setShowResult] = useState(true);
|
||||
const [scanComplete, setScanComplete] = useState(false);
|
||||
const recommendation = getRecommendation(selectedAsset, selectedConcern);
|
||||
const handleScan = () => {
|
||||
setIsScanning(true);
|
||||
setScanProgress(0);
|
||||
setScanComplete(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (isScanning) {
|
||||
interval = setInterval(() => {
|
||||
setScanProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsScanning(false);
|
||||
setScanComplete(true);
|
||||
return 100;
|
||||
}
|
||||
return prev + 4;
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isScanning]);
|
||||
const CurrentAssetIcon = assets.find((a) => a.id === selectedAsset)?.icon || Cpu;
|
||||
return <div id="ndt-advisor-widget" className="bg-white border border-slate-200 rounded-3xl shadow-xl overflow-hidden max-w-6xl mx-auto my-6">
|
||||
<div className="grid lg:grid-cols-12">
|
||||
|
||||
{
|
||||
/* Left Side: Control Center (7 cols) */
|
||||
}
|
||||
<div className="lg:col-span-7 p-6 sm:p-10 border-b lg:border-b-0 lg:border-r border-slate-100 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="w-2.5 h-2.5 bg-orange-500 rounded-full animate-pulse" />
|
||||
<span className="font-mono text-[10px] font-bold text-orange-500 uppercase tracking-widest">
|
||||
Interactive Engineering System
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-heading text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">
|
||||
NDT Strategy & Calibration Configurator
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 font-light mt-2 max-w-xl leading-relaxed">
|
||||
Select an industrial asset category and primary mechanical concern to generate a compliant code procedure recommendation and execute a simulated instrument calibration scan.
|
||||
</p>
|
||||
|
||||
{
|
||||
/* Asset Selector */
|
||||
}
|
||||
<div className="mt-8">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-wider font-heading block mb-3">
|
||||
1. Select Asset Geometry
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{assets.map((asset) => {
|
||||
const IconComp = asset.icon;
|
||||
const isSelected = selectedAsset === asset.id;
|
||||
return <button
|
||||
key={asset.id}
|
||||
onClick={() => {
|
||||
setSelectedAsset(asset.id);
|
||||
setScanComplete(false);
|
||||
}}
|
||||
className={`p-4 rounded-2xl border text-left transition-all duration-200 ${isSelected ? "bg-slate-900 text-white border-slate-900 shadow-md" : "bg-slate-50 text-slate-700 border-slate-200/60 hover:bg-slate-100/70"}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-xl ${isSelected ? "bg-orange-500 text-white" : "bg-slate-200/60 text-slate-600"}`}>
|
||||
<IconComp className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold font-heading">{asset.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Concern Selector */
|
||||
}
|
||||
<div className="mt-6">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-wider font-heading block mb-3">
|
||||
2. Primary Concern or Damage Loop
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{concerns.map((concern) => {
|
||||
const isSelected = selectedConcern === concern.id;
|
||||
return <button
|
||||
key={concern.id}
|
||||
onClick={() => {
|
||||
setSelectedConcern(concern.id);
|
||||
setScanComplete(false);
|
||||
}}
|
||||
className={`p-4 rounded-2xl border text-left transition-all duration-200 ${isSelected ? "bg-orange-500 text-white border-orange-500 shadow-md" : "bg-slate-50 text-slate-700 border-slate-200/60 hover:bg-slate-100/70"}`}
|
||||
>
|
||||
<h4 className="text-xs font-bold font-heading">{concern.name}</h4>
|
||||
<p className={`text-[10px] mt-1 leading-normal ${isSelected ? "text-orange-100" : "text-slate-400"}`}>
|
||||
{concern.description}
|
||||
</p>
|
||||
</button>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Action Footer */
|
||||
}
|
||||
<div className="mt-10 pt-6 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 text-xs font-mono font-bold">
|
||||
i
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-light max-w-xs leading-normal">
|
||||
Recommendations are vetted by our ASNT Level III and API 510/570 planning committees.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleScan}
|
||||
disabled={isScanning}
|
||||
className={`w-full sm:w-auto bg-slate-900 text-white hover:bg-slate-800 disabled:opacity-50 font-bold text-xs px-6 py-3.5 rounded-xl uppercase tracking-wider transition-all duration-200 flex items-center justify-center gap-2 shadow-md`}
|
||||
>
|
||||
{isScanning ? <>
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-white" />
|
||||
<span>Scanning Instrument...</span>
|
||||
</> : <>
|
||||
<Zap className="w-4 h-4 text-orange-400 fill-orange-400 animate-pulse" />
|
||||
<span>Calibrate & Test Probe</span>
|
||||
</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Right Side: Compliance Sheet & Live Scan Screen (5 cols) */
|
||||
}
|
||||
<div className="lg:col-span-5 p-6 sm:p-10 bg-slate-950 text-white flex flex-col justify-between relative overflow-hidden">
|
||||
|
||||
{
|
||||
/* Subtle tech grid background */
|
||||
}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1e293b_1px,transparent_1px),linear-gradient(to_bottom,#1e293b_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] opacity-20 pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 space-y-6">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||||
<span className="font-mono text-xs font-bold text-orange-400 uppercase tracking-widest">
|
||||
Compliance Spec Sheet
|
||||
</span>
|
||||
<span className={`text-[9px] font-mono uppercase tracking-widest px-2 py-0.5 rounded border ${recommendation.dangerLevel === "High" ? "bg-red-500/10 text-red-400 border-red-500/20" : "bg-amber-500/10 text-amber-400 border-amber-500/20"}`}>
|
||||
Critical Level: {recommendation.dangerLevel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Simulated Live Scan Screen */
|
||||
}
|
||||
<div className="bg-slate-900/90 border border-slate-800 rounded-2xl p-5 overflow-hidden font-mono relative">
|
||||
<div className="flex items-center justify-between text-[10px] text-slate-400 mb-4 pb-2 border-b border-slate-800/60">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${isScanning ? "bg-orange-500 animate-ping" : "bg-green-500"}`} />
|
||||
{isScanning ? "SIGNAL ACQUISITION ACTIVE" : "CALIBRATED & READY"}
|
||||
</span>
|
||||
<span>CH_A: 5.0 MHz</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Scan visualization box */
|
||||
}
|
||||
<div className="h-28 flex items-center justify-center relative bg-slate-950 rounded-lg overflow-hidden border border-slate-800/40">
|
||||
|
||||
{
|
||||
/* Simulated wave or scan line */
|
||||
}
|
||||
<AnimatePresence>
|
||||
{isScanning ? <motion.div
|
||||
key="scanning"
|
||||
className="absolute inset-0 flex flex-col justify-center px-4"
|
||||
>
|
||||
{
|
||||
/* Animated wave path */
|
||||
}
|
||||
<svg viewBox="0 0 300 80" className="w-full h-full text-orange-500 stroke-2 fill-none">
|
||||
<path d={`M 0,40 Q 30,20 60,40 T 120,40 T 180,${40 - scanProgress / 3} T 240,40 T 300,40`} className="animate-pulse" stroke="currentColor" />
|
||||
</svg>
|
||||
{
|
||||
/* Scanning laser sweep */
|
||||
}
|
||||
<motion.div
|
||||
className="absolute top-0 bottom-0 w-0.5 bg-orange-400 shadow-[0_0_10px_#f97316] z-10"
|
||||
animate={{ left: ["0%", "100%", "0%"] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
|
||||
/>
|
||||
</motion.div> : scanComplete ? <motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="absolute inset-0 bg-orange-500/5 p-3 flex flex-col justify-between"
|
||||
>
|
||||
<div className="flex items-start justify-between text-[9px]">
|
||||
<span className="text-orange-400 font-bold">⚠️ DETECTED ANOMALY</span>
|
||||
<span className="text-slate-400">POS: 184.2 mm</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Anomaly wave signal with a massive spike */
|
||||
}
|
||||
<svg viewBox="0 0 300 60" className="w-full h-10 text-orange-500 stroke-2 fill-none">
|
||||
<path d="M 0,30 L 80,30 L 100,10 L 115,50 L 130,30 L 160,30 L 175,5 L 185,55 L 195,5 L 210,30 L 300,30" stroke="currentColor" />
|
||||
</svg>
|
||||
|
||||
<div className="text-[10px] text-orange-300 leading-normal font-sans bg-orange-500/10 border border-orange-500/20 rounded p-1.5 mt-1">
|
||||
<strong>{recommendation.simulatedSignalName}:</strong> {recommendation.simulatedSignalDesc}
|
||||
</div>
|
||||
</motion.div> : <div className="text-center text-xs text-slate-500 flex flex-col items-center gap-2">
|
||||
<CurrentAssetIcon className="w-8 h-8 text-slate-600 animate-pulse" />
|
||||
<span>Press Calibrate & Test Probe to capture NDT signals</span>
|
||||
</div>}
|
||||
</AnimatePresence>
|
||||
|
||||
{
|
||||
/* Progress Overlay */
|
||||
}
|
||||
{isScanning && <div className="absolute inset-0 bg-slate-950/80 flex flex-col items-center justify-center p-4">
|
||||
<span className="text-xs font-bold text-orange-400 mb-2 font-mono">SWEEPING ASSET... {scanProgress}%</span>
|
||||
<div className="w-44 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-orange-500" style={{ width: `${scanProgress}%` }} />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Core Recommendation Specs */
|
||||
}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Recommended NDT Technique</span>
|
||||
<p className="text-sm font-bold text-white mt-1 flex items-center gap-1.5">
|
||||
<CheckCircle className="w-4.5 h-4.5 text-orange-500 shrink-0" />
|
||||
{recommendation.technique}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 font-light mt-1">{recommendation.advantage}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pt-2 border-t border-slate-900">
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Procedural Standards</span>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
{recommendation.standards.map((s) => <span key={s} className="font-mono text-[10px] text-orange-300 block bg-orange-500/5 py-0.5 px-1.5 rounded border border-orange-500/10 w-fit">
|
||||
{s}
|
||||
</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Calibration Code</span>
|
||||
<span className="font-mono text-xs font-bold text-slate-200 mt-1 block">
|
||||
{recommendation.techCode}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Prep Work Required */
|
||||
}
|
||||
<div className="pt-4 border-t border-slate-900">
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Mandatory Field Preparations</span>
|
||||
<ul className="mt-2 space-y-1.5 text-xs text-slate-300 font-light list-none">
|
||||
{recommendation.prepRequired.map((p, idx) => <li key={idx} className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-orange-500 mt-1.5 shrink-0" />
|
||||
<span>{p}</span>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-4 border-t border-slate-800 text-center relative z-10">
|
||||
<p className="text-[11px] text-slate-400 font-light">
|
||||
Need custom procedures drafted for this asset configuration?
|
||||
</p>
|
||||
<a
|
||||
href="/contact"
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-bold text-orange-400 hover:text-orange-300 uppercase tracking-wider mt-2 group"
|
||||
>
|
||||
<span>Consult Our Level III Engineers</span>
|
||||
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check, X, Info } from "lucide-react";
|
||||
export default function MethodCard({ method }) {
|
||||
return <motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="bg-white border border-slate-100 p-6 md:p-8 rounded-2xl shadow-lg hover:shadow-xl hover:border-orange-500/20 transition-all duration-300 flex flex-col relative overflow-hidden group hover:-translate-y-1"
|
||||
>
|
||||
{
|
||||
/* Decorative accent top-line */
|
||||
}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-orange-500/10 via-orange-500 to-orange-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{
|
||||
/* Code Badge */
|
||||
}
|
||||
<span className="absolute top-6 right-6 font-mono text-xs font-semibold text-slate-500 bg-slate-50 px-3 py-1 rounded-full border border-slate-100 tracking-wider">
|
||||
METHOD {method.code}
|
||||
</span>
|
||||
|
||||
<h3 className="font-heading text-xl md:text-2xl font-bold text-slate-900 group-hover:text-orange-500 transition-colors duration-200 mt-2 pr-28">
|
||||
{method.name}
|
||||
</h3>
|
||||
<p className="mt-4 text-slate-500 text-sm leading-relaxed font-light">
|
||||
{method.description}
|
||||
</p>
|
||||
|
||||
{method.note && <div className="mt-5 flex gap-3.5 items-start bg-amber-50/40 border-l-4 border-amber-500 p-4 rounded-xl">
|
||||
<Info className="w-4 h-4 text-amber-600 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-amber-800 leading-relaxed font-normal">
|
||||
{method.note}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{(method.advantages || method.limitations) && <div className="grid sm:grid-cols-2 gap-6 mt-6 pt-5 border-t border-slate-50">
|
||||
{method.advantages && <div>
|
||||
<p className="font-heading text-xs font-bold tracking-wider text-slate-900 mb-3.5 uppercase">Advantages</p>
|
||||
<ul className="space-y-2.5">
|
||||
{method.advantages.map((a) => <li key={a} className="text-xs text-slate-600 flex gap-2.5 items-start font-light">
|
||||
<span className="w-5 h-5 rounded-full bg-emerald-50 text-emerald-600 flex items-center justify-center shrink-0 mt-0.5 border border-emerald-100/50">
|
||||
<Check className="w-3 h-3" />
|
||||
</span>
|
||||
<span>{a}</span>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>}
|
||||
{method.limitations && <div>
|
||||
<p className="font-heading text-xs font-bold tracking-wider text-slate-500 mb-3.5 uppercase">Limitations</p>
|
||||
<ul className="space-y-2.5">
|
||||
{method.limitations.map((l) => <li key={l} className="text-xs text-slate-500 flex gap-2.5 items-start font-light">
|
||||
<span className="w-5 h-5 rounded-full bg-rose-50 text-rose-500 flex items-center justify-center shrink-0 mt-0.5 border border-rose-100/50">
|
||||
<X className="w-3 h-3" />
|
||||
</span>
|
||||
<span>{l}</span>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
{method.uses && <div className="mt-6 pt-5 border-t border-slate-50">
|
||||
<p className="font-heading text-[10px] font-bold tracking-wider text-slate-800 mb-3 uppercase">Key Applications</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{method.uses.map((u) => <span
|
||||
key={u}
|
||||
className="text-xs border border-slate-200 bg-slate-50 text-slate-600 px-3.5 py-1.5 rounded-lg hover:border-orange-500/40 hover:text-orange-500 transition-all duration-200 cursor-default"
|
||||
>
|
||||
{u}
|
||||
</span>)}
|
||||
</div>
|
||||
</div>}
|
||||
</motion.div>;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { NavLink, Link, useLocation } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { ChevronDown, Menu, X, Phone, Mail, MapPin, ArrowRight } from "lucide-react";
|
||||
import { serviceCategories } from "../data/content";
|
||||
const navLinkClass = ({ isActive }) => `relative text-sm font-semibold tracking-wide py-1.5 transition-all duration-200 ${isActive ? "text-orange-500 font-bold border-b-2 border-orange-500" : "text-slate-600 hover:text-orange-500"}`;
|
||||
export default function Navbar() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [servicesOpen, setServicesOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 40);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
setServicesOpen(false);
|
||||
}, [location]);
|
||||
return <header className="w-full z-50 flex flex-col transition-all duration-300">
|
||||
{
|
||||
/* Top bar with contact info */
|
||||
}
|
||||
<div
|
||||
className={`bg-slate-950 border-b border-slate-800 text-slate-300 py-2.5 px-5 md:px-8 transition-all duration-300 ${isScrolled ? "h-0 py-0 overflow-hidden opacity-0 border-none" : "h-auto opacity-100"}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-3">
|
||||
<div className="flex flex-wrap justify-center sm:justify-start items-center gap-5 text-xs">
|
||||
<a href="tel:+966571033252" className="flex items-center gap-2 hover:text-orange-500 transition-colors font-medium">
|
||||
<Phone className="w-3.5 h-3.5 text-orange-500 shrink-0" />
|
||||
<span>+966 57 103 3252</span>
|
||||
</a>
|
||||
<a href="mailto:sales@masarndt.com" className="flex items-center gap-2 hover:text-orange-500 transition-colors font-medium">
|
||||
<Mail className="w-3.5 h-3.5 text-orange-500 shrink-0" />
|
||||
<span>sales@masarndt.com</span>
|
||||
</a>
|
||||
<span className="flex items-center gap-2 text-slate-400">
|
||||
<MapPin className="w-3.5 h-3.5 text-orange-500 shrink-0" />
|
||||
<span>Jubail, Saudi Arabia</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs font-medium">
|
||||
<span className="font-bold text-white uppercase text-[9px] tracking-wider bg-slate-900 border border-slate-800 px-2 py-0.5 rounded">
|
||||
ISO 9001:2015 Approved
|
||||
</span>
|
||||
<span className="text-slate-800">|</span>
|
||||
<Link to="/contact" className="text-slate-400 hover:text-orange-500 transition-colors">Support</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Main navigation bar (Sticky) */
|
||||
}
|
||||
<div
|
||||
className={`w-full bg-white transition-all duration-300 ${isScrolled ? "fixed top-0 shadow-lg border-b border-slate-100 py-3.5" : "relative py-5 border-b border-slate-100"}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8 flex items-center justify-between">
|
||||
{
|
||||
/* Brand Logo */
|
||||
}
|
||||
<Link to="/" className="flex items-center gap-2.5 shrink-0">
|
||||
<span className="w-5 h-5 rounded bg-orange-500 shadow-md flex items-center justify-center font-bold text-white text-[10px]" aria-hidden="true">M</span>
|
||||
<span className="font-heading font-extrabold uppercase tracking-wide text-slate-900 text-xl sm:text-2xl">
|
||||
Masar<span className="text-orange-500 font-black"> NDT</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{
|
||||
/* Desktop Navigation Links */
|
||||
}
|
||||
<nav className="hidden lg:flex items-center gap-8">
|
||||
<NavLink to="/" className={navLinkClass} end>Home</NavLink>
|
||||
<NavLink to="/about" className={navLinkClass}>About</NavLink>
|
||||
|
||||
{
|
||||
/* Services Dropdown */
|
||||
}
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setServicesOpen(true)}
|
||||
onMouseLeave={() => setServicesOpen(false)}
|
||||
>
|
||||
<button
|
||||
className="text-sm font-semibold tracking-wide text-slate-600 hover:text-orange-500 flex items-center gap-1 transition-colors py-1.5"
|
||||
onClick={() => setServicesOpen(!servicesOpen)}
|
||||
>
|
||||
Services
|
||||
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${servicesOpen ? "rotate-180 text-orange-500" : "text-slate-400"}`} />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{servicesOpen && <motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute left-0 top-full mt-2 w-80 bg-white border border-slate-100 shadow-xl p-2 rounded-xl z-50"
|
||||
>
|
||||
<div className="grid gap-1">
|
||||
{serviceCategories.map((s) => <NavLink
|
||||
key={s.id}
|
||||
to={s.path}
|
||||
className={({ isActive }) => `group flex flex-col p-3 rounded-lg transition-all duration-200 ${isActive ? "bg-slate-50 border-l-4 border-orange-500" : "hover:bg-slate-50"}`}
|
||||
onClick={() => setServicesOpen(false)}
|
||||
>
|
||||
<span className="text-[9px] font-bold text-orange-500 uppercase tracking-widest">{s.tag}</span>
|
||||
<span className="text-sm font-bold text-slate-900 group-hover:text-orange-500 transition-colors flex items-center mt-0.5 font-heading">
|
||||
{s.title}
|
||||
<ArrowRight className="w-4 h-4 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all ml-auto text-orange-500" />
|
||||
</span>
|
||||
</NavLink>)}
|
||||
</div>
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<NavLink to="/industries" className={navLinkClass}>Industries</NavLink>
|
||||
<NavLink to="/projects" className={navLinkClass}>Projects</NavLink>
|
||||
<NavLink to="/gallery" className={navLinkClass}>Gallery</NavLink>
|
||||
<NavLink to="/contact" className={navLinkClass}>Contact</NavLink>
|
||||
</nav>
|
||||
|
||||
{
|
||||
/* Right Action Button & Mobile Trigger */
|
||||
}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/contact"
|
||||
className="hidden sm:inline-block bg-orange-500 hover:bg-orange-600 text-white font-bold text-xs px-6 py-3.5 rounded-xl tracking-wider uppercase shadow-md hover:shadow-orange-500/10 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-200"
|
||||
>
|
||||
Get a Quote
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="lg:hidden p-2.5 text-slate-950 hover:text-orange-500 border border-slate-100 rounded-xl transition-colors"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-expanded={open}
|
||||
>
|
||||
{open ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* Mobile Drawer Menu */
|
||||
}
|
||||
<AnimatePresence>
|
||||
{open && <motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="lg:hidden w-full bg-white border-b border-slate-100 px-6 py-6 flex flex-col gap-4 overflow-y-auto max-h-[calc(100vh-80px)] z-40 absolute top-full left-0 shadow-2xl"
|
||||
>
|
||||
<NavLink to="/" className="text-sm font-bold text-slate-800 hover:text-orange-500 font-heading" end onClick={() => setOpen(false)}>Home</NavLink>
|
||||
<NavLink to="/about" className="text-sm font-bold text-slate-800 hover:text-orange-500 font-heading" onClick={() => setOpen(false)}>About</NavLink>
|
||||
|
||||
<div className="h-px bg-slate-100 my-1" />
|
||||
|
||||
<p className="text-[10px] font-bold text-orange-500 uppercase tracking-widest font-heading">Services</p>
|
||||
<div className="grid gap-2 pl-3 border-l-2 border-orange-500 ml-1">
|
||||
{serviceCategories.map((s) => <NavLink
|
||||
key={s.id}
|
||||
to={s.path}
|
||||
className={({ isActive }) => `text-sm font-bold transition-colors py-1 font-heading ${isActive ? "text-orange-500" : "text-slate-800 hover:text-orange-500"}`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{s.title}
|
||||
</NavLink>)}
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-slate-100 my-1" />
|
||||
|
||||
<NavLink to="/industries" className="text-sm font-bold text-slate-800 hover:text-orange-500 font-heading" onClick={() => setOpen(false)}>Industries</NavLink>
|
||||
<NavLink to="/projects" className="text-sm font-bold text-slate-800 hover:text-orange-500 font-heading" onClick={() => setOpen(false)}>Projects</NavLink>
|
||||
<NavLink to="/gallery" className="text-sm font-bold text-slate-800 hover:text-orange-500 font-heading" onClick={() => setOpen(false)}>Gallery</NavLink>
|
||||
<NavLink to="/contact" className="text-sm font-bold text-slate-800 hover:text-orange-500 font-heading" onClick={() => setOpen(false)}>Contact</NavLink>
|
||||
|
||||
<Link
|
||||
to="/contact"
|
||||
className="mt-4 text-center bg-orange-500 hover:bg-orange-600 text-white font-bold text-xs py-4 rounded-xl tracking-wider uppercase shadow-md transition-all font-heading"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Get a Quote
|
||||
</Link>
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</header>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { motion } from "motion/react";
|
||||
export default function SectionHeader({ tag, title, description, align = "left", dark = false }) {
|
||||
const isCenter = align === "center";
|
||||
return <motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className={`max-w-4xl ${isCenter ? "mx-auto text-center flex flex-col items-center" : ""} mb-12`}
|
||||
>
|
||||
{tag && <p className="font-mono text-[10px] font-bold tracking-widest text-orange-500 mb-3 flex items-center gap-2 uppercase">
|
||||
<span className="w-4 h-px bg-orange-500/40 inline-block" />
|
||||
{tag}
|
||||
</p>}
|
||||
<h2 className={`font-heading text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight leading-[1.1] ${dark ? "text-white" : "text-slate-900"}`}>
|
||||
{title}
|
||||
</h2>
|
||||
{description && <p className={`mt-4 leading-relaxed text-sm md:text-base max-w-2xl font-light ${dark ? "text-slate-300" : "text-slate-500"}`}>
|
||||
{description}
|
||||
</p>}
|
||||
</motion.div>;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowRight, ScanSearch, Zap, Activity, DatabaseZap, UserCheck } from "lucide-react";
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.08
|
||||
}
|
||||
}
|
||||
};
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } }
|
||||
};
|
||||
const serviceIcons = [ScanSearch, Zap, Activity, DatabaseZap, UserCheck];
|
||||
export default function ServiceCategoryGrid({ categories }) {
|
||||
return <motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
{categories.map((s, idx) => {
|
||||
const Icon = serviceIcons[idx] || ScanSearch;
|
||||
return <motion.div key={s.id} variants={itemVariants}>
|
||||
<Link
|
||||
to={s.path}
|
||||
className="group block bg-white border border-slate-100 p-8 flex flex-col justify-between min-h-[300px] rounded-2xl transition-all duration-300 relative overflow-hidden shadow-md hover:shadow-xl hover:border-orange-500/20 hover:-translate-y-1"
|
||||
>
|
||||
{
|
||||
/* Top Accent line */
|
||||
}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-orange-500/20 via-orange-500 to-orange-500/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-12 h-12 rounded-xl bg-orange-500/10 text-orange-600 flex items-center justify-center shadow-sm group-hover:scale-110 group-hover:rotate-3 transition-all duration-300">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<p className="font-mono text-[10px] font-bold tracking-wider text-orange-500 bg-orange-500/5 px-2.5 py-1 rounded-full">{s.tag}</p>
|
||||
</div>
|
||||
|
||||
<h3 className="font-heading text-xl font-bold mt-6 text-slate-900 group-hover:text-orange-500 transition-colors duration-300">
|
||||
{s.title}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 group-hover:text-slate-600 mt-3 leading-relaxed font-light transition-colors duration-300">
|
||||
{s.summary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-5 border-t border-slate-50 flex items-center justify-between relative z-10">
|
||||
<span className="text-xs font-bold text-slate-800 group-hover:text-orange-500 transition-colors duration-200 uppercase tracking-wider">
|
||||
View division scope
|
||||
</span>
|
||||
<div className="w-8 h-8 rounded-full bg-slate-50 text-slate-700 flex items-center justify-center group-hover:bg-orange-500 group-hover:text-white transition-all duration-300 shadow-sm">
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>;
|
||||
})}
|
||||
</motion.div>;
|
||||
}
|
||||
Reference in New Issue
Block a user