mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
fix ui
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchRequirementSatisfied,
|
||||
AdminSetupAlertResponse,
|
||||
} from "@/record-management/services/api/alertService";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export default function AdminSetupAlert({
|
||||
unitId: propUnitId,
|
||||
}: {
|
||||
unitId?: string;
|
||||
}) {
|
||||
const [unitId, setUnitId] = useState<string>(propUnitId || "");
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
// Listen for unitChanged events from Content Management.
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<string>).detail;
|
||||
queueMicrotask(() => setUnitId(detail));
|
||||
};
|
||||
window.addEventListener("unitChanged", handler);
|
||||
return () => window.removeEventListener("unitChanged", handler);
|
||||
}, []);
|
||||
|
||||
// TanStack Query for fetching requirements
|
||||
const { data, isLoading, refetch } = useQuery<AdminSetupAlertResponse>({
|
||||
queryKey: ["admin-setup-requirements", unitId],
|
||||
queryFn: () => fetchRequirementSatisfied(unitId),
|
||||
enabled: !!unitId,
|
||||
});
|
||||
|
||||
const requirements = data?.requirements;
|
||||
|
||||
// Optional: refetch automatically on unit change (real-time trigger)
|
||||
useEffect(() => {
|
||||
if (unitId) {
|
||||
refetch();
|
||||
}
|
||||
}, [unitId, refetch]);
|
||||
|
||||
// Hide if no requirements or all are satisfied
|
||||
if (!requirements || Object.values(requirements).every(Boolean)) return null;
|
||||
|
||||
if (!visible) return null; // hide after dismiss
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-6 p-2 border rounded bg-amber-50 shadow-2xl top-10 left-1/2 -translate-x-1/2 z-[1000] w-full">
|
||||
{/* Title */}
|
||||
<h3 className="font-semibold text-amber-700 whitespace-nowrap">
|
||||
Finish site setup
|
||||
</h3>
|
||||
|
||||
{/* Messages inline */}
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-slate-600">Loading requirements...</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-6 text-sm text-slate-600">
|
||||
{!requirements.hasFooter && <span>⚠️ Missing Footer</span>}
|
||||
{!requirements.hasSeal && <span>⚠️ Missing Seal</span>}
|
||||
{!requirements.hasHeader && <span>⚠️ Missing Header</span>}
|
||||
{!requirements.internalPrefix && (
|
||||
<span>⚠️ Missing Internal Prefix</span>
|
||||
)}
|
||||
{!requirements.externalPrefix && (
|
||||
<span>⚠️ Missing External Prefix</span>
|
||||
)}
|
||||
{!requirements.internalSuffix && (
|
||||
<span>⚠️ Missing Internal Suffix</span>
|
||||
)}
|
||||
{!requirements.externalSuffix && (
|
||||
<span>⚠️ Missing External Suffix</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setVisible(false)}
|
||||
className="absolute top-2 right-2 text-amber-700 hover:text-red-600">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
FaUpload,
|
||||
FaCheckCircle,
|
||||
FaCopy,
|
||||
FaExclamationCircle,
|
||||
FaArrowLeft,
|
||||
} from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import Header from "./Header";
|
||||
import Footer from "./Footer";
|
||||
import {
|
||||
submitComplaint,
|
||||
ComplaintPayload,
|
||||
FileInfo,
|
||||
} from "../../shared/services/complaintService";
|
||||
|
||||
const ComplaintForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disclaimerAccepted, setDisclaimerAccepted] = useState(false);
|
||||
const [submissionSuccess, setSubmissionSuccess] = useState(false);
|
||||
const [complaintId, setComplaintId] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [formValues, setFormValues] = useState({
|
||||
fullName: "",
|
||||
subCity: "",
|
||||
woreda: "",
|
||||
houseNumber: "",
|
||||
phone: "",
|
||||
institution: "",
|
||||
complaintDetails: "",
|
||||
complaintWant: "",
|
||||
complaintPlace: "",
|
||||
});
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setFormValues({ ...formValues, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
setFiles(Array.from(e.target.files));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!disclaimerAccepted) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Prepare file info if files are uploaded
|
||||
let fileInfo: FileInfo | undefined;
|
||||
if (files.length > 0) {
|
||||
const file = files[0]; // Take the first file for now
|
||||
fileInfo = {
|
||||
fileName: file.name,
|
||||
contentType: file.type,
|
||||
size: file.size,
|
||||
originalname: file.name,
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare the complaint payload
|
||||
const payload: ComplaintPayload = {
|
||||
fullName: formValues.fullName,
|
||||
phoneNumber: formValues.phone,
|
||||
subCity: formValues.subCity,
|
||||
woreda: formValues.woreda,
|
||||
houseNumber: formValues.houseNumber,
|
||||
institution: formValues.institution,
|
||||
complaintPlace: formValues.complaintPlace,
|
||||
complaintDetail: formValues.complaintDetails,
|
||||
desiredResolution: formValues.complaintWant,
|
||||
fileInfo,
|
||||
};
|
||||
|
||||
// Submit the complaint
|
||||
const response = await submitComplaint(payload);
|
||||
|
||||
if (response.data) {
|
||||
setComplaintId(response.data.complaintNumber || response.data.id);
|
||||
setSubmissionSuccess(true);
|
||||
toast.success(t("complaint.success"));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Error submitting complaint:", err);
|
||||
const errorMessage =
|
||||
err.response?.data?.message || err.message || t("complaint.error");
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white flex flex-col pt-16 md:pt-20">
|
||||
{/* Hero Section */}
|
||||
<div className="bg-primary py-16 text-center text-white relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10"></div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 relative z-10 animate-fade-in">
|
||||
{t("complaint.title")}
|
||||
</h1>
|
||||
<p className="max-w-2xl mx-auto text-lg opacity-90 relative z-10">
|
||||
{t("complaint.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Form */}
|
||||
<div className="flex-1 flex justify-center p-6">
|
||||
<div className="w-full max-w-3xl bg-white shadow-2xl rounded-2xl p-8 relative border border-gray-100">
|
||||
{/* Floating circles */}
|
||||
<div className="absolute -top-6 -left-6 w-16 h-16 bg-blue-100 rounded-full opacity-30 animate-pulse"></div>
|
||||
<div className="absolute -bottom-6 -right-6 w-24 h-24 bg-blue-100 rounded-full opacity-30 animate-ping"></div>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-blue-800 mb-8 text-center">
|
||||
{t("complaint.formTitle")}
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaExclamationCircle className="text-red-500" />
|
||||
<span className="text-red-700 font-medium">
|
||||
{t("complaint.error")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-red-600 text-sm mt-1">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{submissionSuccess ? (
|
||||
<div className="text-center animate-fade-in">
|
||||
<FaCheckCircle className="text-primary-500 text-5xl mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
{t("complaint.success")}
|
||||
</h3>
|
||||
<p className="mb-4 text-gray-600">{t("complaint.keepId")}</p>
|
||||
<div className="bg-gray-100 border rounded-lg p-4 flex justify-center items-center gap-3">
|
||||
<span className="font-bold text-blue-700">{complaintId}</span>
|
||||
<button
|
||||
className="p-2 rounded hover:bg-gray-200"
|
||||
onClick={() => navigator.clipboard.writeText(complaintId)}
|
||||
>
|
||||
<FaCopy />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 mt-6">
|
||||
<button
|
||||
onClick={() => setSubmissionSuccess(false)}
|
||||
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg shadow hover:bg-primary/90 transition"
|
||||
>
|
||||
{t("complaint.close")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/complaints")}
|
||||
className="px-6 py-3 bg-gray-300 text-gray-800 rounded-lg shadow hover:bg-gray-400 transition flex items-center gap-2"
|
||||
>
|
||||
<FaArrowLeft /> {t("complaint.back")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-6 animate-fade-in"
|
||||
>
|
||||
{/* Inputs */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
value={formValues.fullName}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.fullName")}
|
||||
required
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formValues.phone}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.phone")}
|
||||
required
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="subCity"
|
||||
value={formValues.subCity}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.subCity")}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="woreda"
|
||||
value={formValues.woreda}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.woreda")}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="houseNumber"
|
||||
value={formValues.houseNumber}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.houseNumber")}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="institution"
|
||||
value={formValues.institution}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.institution")}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="complaintPlace"
|
||||
value={formValues.complaintPlace}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.place")}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
name="complaintDetails"
|
||||
value={formValues.complaintDetails}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.details")}
|
||||
rows={4}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
></textarea>
|
||||
|
||||
<textarea
|
||||
name="complaintWant"
|
||||
value={formValues.complaintWant}
|
||||
onChange={handleChange}
|
||||
placeholder={t("complaint.want")}
|
||||
rows={4}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
|
||||
></textarea>
|
||||
|
||||
{/* File Upload */}
|
||||
{/* <div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t("complaint.evidence")}
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-lg file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-blue-50 file:text-blue-700
|
||||
hover:file:bg-blue-100"
|
||||
/>
|
||||
<FaUpload className="text-gray-400" />
|
||||
</div>
|
||||
{files.length > 0 && (
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
{t("complaint.selectedFiles")}:{" "}
|
||||
{files.map((f) => f.name).join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
{/* Disclaimer */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FaExclamationCircle className="text-red-500" />
|
||||
<span className="font-semibold text-red-700">
|
||||
{t("complaint.disclaimerTitle")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700">
|
||||
{t("complaint.disclaimerText")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disclaimerAccepted}
|
||||
onChange={(e) => setDisclaimerAccepted(e.target.checked)}
|
||||
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{t("complaint.accept")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Submit + Back */}
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/complaints")}
|
||||
className="flex items-center gap-2 px-5 py-3 bg-gray-300 text-gray-800 rounded-lg shadow hover:bg-gray-400 transition"
|
||||
>
|
||||
<FaArrowLeft /> {t("complaint.back")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!disclaimerAccepted || loading}
|
||||
className={`px-6 py-3 rounded-lg text-white font-semibold transition flex items-center justify-center gap-2
|
||||
${
|
||||
loading || !disclaimerAccepted
|
||||
? "bg-gray-400 cursor-not-allowed"
|
||||
: "bg-primary hover:bg-primary-700"
|
||||
}`}
|
||||
>
|
||||
{loading && (
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
)}
|
||||
{loading
|
||||
? t("complaint.submitting")
|
||||
: t("complaint.submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplaintForm;
|
||||
@@ -0,0 +1,229 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
const FeaturesSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: t("landingPage.outgoingRecords"),
|
||||
description: t("landingPage.outgoingRecordsDesc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
color: "bg-primary",
|
||||
},
|
||||
{
|
||||
name: t("landingPage.incomingRecords"),
|
||||
description: t("landingPage.incomingRecordsDesc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
color: "bg-primary-500",
|
||||
},
|
||||
{
|
||||
name: t("landingPage.approvalWorkflows"),
|
||||
description: t("landingPage.approvalWorkflowsDesc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
color: "bg-[#3B82F6]",
|
||||
},
|
||||
{
|
||||
name: t("landingPage.interactiveDashboard"),
|
||||
description: t("landingPage.interactiveDashboardDesc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
color: "bg-[#8B5CF6]",
|
||||
},
|
||||
{
|
||||
name: t("landingPage.organizedFolders"),
|
||||
description: t("landingPage.organizedFoldersDesc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
color: "bg-[#EC4899]",
|
||||
},
|
||||
{
|
||||
name: t("landingPage.securityAccessControl"),
|
||||
description: t("landingPage.securityAccessControlDesc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
color: "bg-[#F59E0B]",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="py-16 bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="lg:text-center mb-16"
|
||||
>
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary/10 text-primary">
|
||||
{t("landingPage.powerfulFeaturesTitle")}
|
||||
</span>
|
||||
<h2 className="mt-4 text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
|
||||
<span className="block">
|
||||
{t("landingPage.powerfulFeaturesSubtitle")}
|
||||
</span>
|
||||
<span className="block text-primary">
|
||||
{t("landingPage.documentWorkflow")}
|
||||
</span>
|
||||
</h2>
|
||||
<p className="mt-6 max-w-3xl text-xl text-gray-600 dark:text-gray-300 lg:mx-auto">
|
||||
{t("landingPage.powerfulFeaturesDesc")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
onHoverStart={() => setHoveredIndex(index)}
|
||||
onHoverEnd={() => setHoveredIndex(null)}
|
||||
className="relative"
|
||||
>
|
||||
<div
|
||||
className={`absolute -inset-0.5 rounded-xl ${
|
||||
feature.color
|
||||
} blur opacity-75 transition duration-500 ${
|
||||
hoveredIndex === index ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
></div>
|
||||
<div className="relative bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 h-full transition-all duration-300 hover:border-primary/30">
|
||||
<div
|
||||
className={`flex items-center justify-center h-12 w-12 rounded-lg ${feature.color} text-white mb-4`}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{feature.name}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">{feature.description}</p>
|
||||
<div className="mt-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="mt-20 bg-gradient-to-r from-primary to-primary-500 rounded-2xl shadow-xl overflow-hidden"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto py-12 px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
{[
|
||||
{ number: "95%", label: t("landingPage.fasterApproval") },
|
||||
{ number: "10x", label: t("landingPage.auditReady") },
|
||||
{ number: "100%", label: t("landingPage.organizedRecords") },
|
||||
].map((stat, index) => (
|
||||
<div key={index} className="px-6 py-8">
|
||||
<p className="text-4xl font-extrabold text-white">
|
||||
{stat.number}
|
||||
</p>
|
||||
<p className="mt-2 text-lg font-medium text-white/90">
|
||||
{stat.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesSection;
|
||||
@@ -0,0 +1,172 @@
|
||||
import React, { useState } from "react";
|
||||
import { FaExclamationCircle, FaArrowLeft } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Header from "./Header";
|
||||
import Footer from "./Footer";
|
||||
import { getComplaintById } from "../../shared/services/complaintService";
|
||||
|
||||
const FollowCompliant = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchButton, setSearchButton] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchId, setSearchId] = useState("");
|
||||
const [formValues, setFormValues] = useState({
|
||||
complaintNumber: "",
|
||||
status: "",
|
||||
createdAt: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
const handleSubmit = async (id: string) => {
|
||||
try {
|
||||
setLoading(true); // start spinner
|
||||
const response = await getComplaintById(id);
|
||||
|
||||
if (response) {
|
||||
setFormValues({
|
||||
complaintNumber: response.data.complaintNumber || "",
|
||||
status: response.data.status || "",
|
||||
createdAt: response.data.createdAt || "",
|
||||
message: response.data.message || "",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(t("complaint.errorMsg"));
|
||||
} finally {
|
||||
setLoading(false); // stop spinner
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white flex flex-col pt-16 md:pt-20">
|
||||
{/* Hero Section */}
|
||||
<div className="bg-primary py-16 text-center text-white relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10"></div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 relative z-10 animate-fade-in">
|
||||
{t("complaint.followTitle")}
|
||||
</h1>
|
||||
<p className="max-w-2xl mx-auto text-lg opacity-90 relative z-10">
|
||||
{t("complaint.followSubtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Form */}
|
||||
<div className="flex-1 flex justify-center p-6">
|
||||
<div className="w-full max-w-3xl bg-white shadow-2xl rounded-2xl p-8 relative border border-gray-100">
|
||||
{/* Floating circles */}
|
||||
<div className="absolute -top-6 -left-6 w-16 h-16 bg-blue-100 rounded-full opacity-30 animate-pulse"></div>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-blue-800 mb-8 text-center">
|
||||
{t("complaint.followTitle")}
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaExclamationCircle className="text-red-500" />
|
||||
<span className="text-red-700 font-medium">
|
||||
{t("complaint.error")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-red-600 text-sm mt-1">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<form className="space-y-6 animate-fade-in">
|
||||
{/* View Fields */}
|
||||
<div className="flex gap-2">
|
||||
<label htmlFor="complaintId" className="self-center">
|
||||
{t("complaint.compliantId")}
|
||||
</label>
|
||||
<input
|
||||
id="complaintId"
|
||||
type="text"
|
||||
value={searchId}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchId(value);
|
||||
setSearchButton(value.trim() !== "");
|
||||
}}
|
||||
placeholder={t("complaint.enterComNum")}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={!searchButton || loading}
|
||||
onClick={() => handleSubmit(searchId)}
|
||||
className={`flex items-center justify-center gap-2 px-4 py-2 rounded transition ${
|
||||
searchButton && !loading
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
t("userRecord.Search")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<p className="w-full p-3 border rounded-lg bg-gray-50 text-gray-700">
|
||||
{formValues.complaintNumber || t("complaint.complaintNo")}
|
||||
</p>
|
||||
<p className="w-full p-3 border rounded-lg bg-gray-50 text-gray-700">
|
||||
{formValues.status || t("userRecord.Status")}
|
||||
</p>
|
||||
<p className="w-full p-3 border rounded-lg bg-gray-50 text-gray-700">
|
||||
{formValues.createdAt || t("userRecord.createdAt")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Message in full width */}
|
||||
<div className="mt-6">
|
||||
<p className="w-full min-h-[120px] p-3 border rounded-lg bg-gray-50 text-gray-700 whitespace-pre-line">
|
||||
{formValues.message || t("complaint.message")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Back */}
|
||||
<div className="flex justify-end items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/")}
|
||||
className="flex items-center cursor-pointer gap-2 px-5 py-3 bg-gray-300 text-gray-800 rounded-lg shadow hover:bg-gray-400 transition"
|
||||
>
|
||||
<FaArrowLeft /> {t("complaint.back")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FollowCompliant;
|
||||
374
apps/edr-freight-web/backoffice/src/layout/components/Footer.tsx
Normal file
374
apps/edr-freight-web/backoffice/src/layout/components/Footer.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
Tooltip,
|
||||
} from "@/shared/common/ui/tooltip";
|
||||
import { motion } from "framer-motion";
|
||||
import { Share2 as Facebook, Globe, Globe as Linkedin, Mail, MapPin, Phone } from "lucide-react";
|
||||
import { FaTelegramPlane, FaTiktok } from "react-icons/fa";
|
||||
import { FaXTwitter } from "react-icons/fa6";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTenantConfig } from "@/layout/components/TenantConfig";
|
||||
|
||||
const Footer = () => {
|
||||
const [activeTab, setActiveTab] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { config: tenantConfig } = useTenantConfig();
|
||||
const defaultFooterData = {
|
||||
address: "XQXP+J6C Lingo Tower, Namibia St, Addis Ababa",
|
||||
phone: "+251-955-232323",
|
||||
email: "sales@triaplc.com",
|
||||
};
|
||||
|
||||
const footerData = tenantConfig?.footerData || defaultFooterData;
|
||||
const navItems = [
|
||||
{ id: "overview", label: t("landingPage.overview") },
|
||||
{ id: "features", label: t("landingPage.features") },
|
||||
{ id: "workflow", label: t("landingPage.workflow") },
|
||||
{ id: "howto", label: t("landingPage.manual") },
|
||||
];
|
||||
const navigateTo = "/";
|
||||
const handleAddressClick = () => {
|
||||
const address =
|
||||
footerData.address || "XQXP+J6C Lingo Tower, Namibia St, Addis Ababa";
|
||||
const query = encodeURIComponent(address);
|
||||
window.open(
|
||||
`https://www.google.com/maps/search/?api=1&query=${query}`,
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
};
|
||||
const handleEmailClick = () => {
|
||||
const email = footerData.email || "sales@triaplc.com";
|
||||
const subject = "";
|
||||
const body = "";
|
||||
const gmailParams = new URLSearchParams();
|
||||
gmailParams.append("to", email);
|
||||
if (subject) gmailParams.append("su", subject);
|
||||
if (body) gmailParams.append("body", body);
|
||||
|
||||
const gmailUrl = `https://mail.google.com/mail/?view=cm&fs=1&${gmailParams.toString()}`;
|
||||
const mailtoUrl = `mailto:${email}?subject=${encodeURIComponent(subject || "")}&body=${encodeURIComponent(body || "")}`;
|
||||
|
||||
window.open(gmailUrl, "_blank", "noopener noreferrer");
|
||||
|
||||
setTimeout(() => {
|
||||
window.open(mailtoUrl, "_blank", "noopener noreferrer");
|
||||
}, 500);
|
||||
};
|
||||
const socials = tenantConfig?.socials || {};
|
||||
const socialLinks = {
|
||||
facebook: socials.facebook || "https://web.facebook.com/Triaplc",
|
||||
twitter: socials.twitter || "https://x.com/Triaplc",
|
||||
linkedin: socials.linkedin || "https://www.linkedin.com/company/triaplc",
|
||||
telegram: socials.telegram || "",
|
||||
tiktok: socials.tiktok || "",
|
||||
website: socials.website || "https://triaplc.com/",
|
||||
};
|
||||
const optionalSocialLinks = [
|
||||
{
|
||||
href: socialLinks.telegram,
|
||||
label: t("landingPage.telegram"),
|
||||
icon: FaTelegramPlane,
|
||||
},
|
||||
{
|
||||
href: socialLinks.tiktok,
|
||||
label: t("landingPage.tiktok"),
|
||||
icon: FaTiktok,
|
||||
},
|
||||
].filter((item) => item.href);
|
||||
|
||||
return (
|
||||
<footer className="bg-gradient-to-b from-gray-900 to-gray-800 dark:bg-gradient-to-b dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
|
||||
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div className="space-y-8 xl:col-span-1">
|
||||
<div className="flex items-center">
|
||||
{tenantConfig?.logo ? (
|
||||
<div className="relative group inline-flex">
|
||||
{/* Glow */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 group-hover:opacity-100 transition-all duration-500 scale-110" />
|
||||
|
||||
{/* Footer Logo Wrapper */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.04 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
transition={{ type: "spring", stiffness: 220, damping: 16 }}
|
||||
className="relative inline-flex items-center justify-center cursor-pointer group"
|
||||
onClick={() => (window.location.href = "/")}
|
||||
title={t("nav.homePage")}
|
||||
>
|
||||
<motion.img
|
||||
src={tenantConfig.logo}
|
||||
alt="Footer Logo"
|
||||
className="
|
||||
h-35
|
||||
w-auto
|
||||
object-contain
|
||||
group-hover:scale-105
|
||||
transition-transform
|
||||
duration-300
|
||||
"
|
||||
onError={(e) => {
|
||||
const fallback =
|
||||
e.currentTarget.parentElement?.querySelector(
|
||||
".footer-logo-fallback",
|
||||
);
|
||||
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden");
|
||||
}
|
||||
|
||||
e.currentTarget.remove();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Fallback when logo fails to load */}
|
||||
<div
|
||||
className="
|
||||
footer-logo-fallback
|
||||
hidden
|
||||
h-20
|
||||
w-20
|
||||
items-center
|
||||
justify-center
|
||||
rounded-full
|
||||
bg-primary
|
||||
text-primary-foreground
|
||||
font-bold
|
||||
text-2xl
|
||||
pointer-events-none
|
||||
"
|
||||
>
|
||||
{tenantConfig?.organizationName
|
||||
?.split(/[\s\-–—]+/)
|
||||
.filter(Boolean)
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase() || "SO"}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-white font-bold text-xl">
|
||||
SO
|
||||
</div>
|
||||
)}
|
||||
<span className="ml-3 text-2xl font-bold text-white">
|
||||
{tenantConfig?.organizationName || t("landingPage.smartOffice")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-300 text-lg max-h-20 overflow-y-auto pr-2">
|
||||
{tenantConfig?.footerText ||
|
||||
t("landingPage.digitalTransformation")}
|
||||
</p>
|
||||
<div className="flex space-x-6">
|
||||
<a
|
||||
href={socialLinks.facebook}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-primary transition-colors duration-300"
|
||||
>
|
||||
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Facebook className="h-6 w-6 hover:text-primary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>{t("landingPage.facebook")}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<span className="sr-only">{t("landingPage.facebook")}</span>
|
||||
</li>
|
||||
</a>
|
||||
<a
|
||||
href={socialLinks.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-primary transition-colors duration-300"
|
||||
>
|
||||
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FaXTwitter className="h-6 w-6 hover:text-primary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>{t("landingPage.twitter")}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<span className="sr-only">{t("landingPage.twitter")}</span>
|
||||
</li>
|
||||
</a>
|
||||
<a
|
||||
href={socialLinks.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-primary transition-colors duration-300"
|
||||
>
|
||||
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Linkedin className="h-6 w-6 hover:text-primary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>{t("landingPage.linkedIn")}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<span className="sr-only">{t("landingPage.linkedIn")}</span>
|
||||
</li>
|
||||
</a>
|
||||
{optionalSocialLinks.map(({ href, label, icon: Icon }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-primary transition-colors duration-300"
|
||||
>
|
||||
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Icon className="h-6 w-6 hover:text-primary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>{label}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<span className="sr-only">{label}</span>
|
||||
</li>
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href={socialLinks.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-primary transition-colors duration-300"
|
||||
>
|
||||
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Globe className="h-6 w-6 hover:text-primary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>{t("landingPage.website")}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<span className="sr-only">{t("landingPage.website")}</span>
|
||||
</li>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 xl:mt-0 xl:col-span-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-300 tracking-wider uppercase">
|
||||
{t("landingPage.contactUs")}
|
||||
</h3>
|
||||
<ul className="mt-4 space-y-4">
|
||||
<li
|
||||
onClick={handleEmailClick}
|
||||
title={footerData.email || "sales@triaplc.com"}
|
||||
className="flex items-center space-x-3 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer"
|
||||
>
|
||||
<Mail className="h-5 w-5 hover:text-primary" />
|
||||
<span className="font-medium">
|
||||
{footerData.email || "sales@triaplc.com"}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center space-x-3 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<Phone className="h-5 w-5 hover:text-primary" />
|
||||
<span className="font-medium">
|
||||
{footerData.phone || "+251-955-232323"}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
onClick={handleAddressClick}
|
||||
title={
|
||||
footerData.address ||
|
||||
"XQXP+J6C Lingo Tower, Namibia St, Addis Ababa"
|
||||
}
|
||||
className="flex items-center space-x-3 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer"
|
||||
>
|
||||
<MapPin className="h-5 w-5 hover:text-primary" />
|
||||
<span className="font-medium">
|
||||
{footerData.address || t("landingPage.address")}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-300 tracking-wider uppercase">
|
||||
{t("landingPage.quickLinks")}
|
||||
</h3>
|
||||
<ul className="mt-4 space-y-4">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
setActiveTab(item.id);
|
||||
const el = document.getElementById(item.id);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`cursor-pointer text-base text-gray-400 hover:text-primary transition-colors duration-300 flex items-center ${
|
||||
activeTab === item.id
|
||||
? "text-gray-400"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
{activeTab === item.id && (
|
||||
<motion.div
|
||||
layoutId="activeTabIndicator"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
|
||||
transition={{
|
||||
type: "spring",
|
||||
bounce: 0.2,
|
||||
duration: 0.6,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 border-t border-gray-700 pt-8 flex flex-col items-center justify-center text-center">
|
||||
<p className="text-base text-gray-400">
|
||||
© {t("landingPage.copyright")}
|
||||
{new Date().getFullYear()} {t("landingPage.rightsReserved")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
703
apps/edr-freight-web/backoffice/src/layout/components/Header.tsx
Normal file
703
apps/edr-freight-web/backoffice/src/layout/components/Header.tsx
Normal file
@@ -0,0 +1,703 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { ExternalPortal } from "@/external-portal/components/External-Portal-Navigation/PortalHeader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/common/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
ChevronDown,
|
||||
FileText,
|
||||
Search,
|
||||
Info,
|
||||
Menu,
|
||||
X,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import { useUser } from "@/shared/context/UserContext";
|
||||
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
||||
import { useTenantConfig, resolveModuleConfig } from "@/layout/components/TenantConfig";
|
||||
import {
|
||||
UI_LANGUAGE_OPTIONS,
|
||||
resolveUiLanguage,
|
||||
} from "@/shared/i18n/uiLanguages";
|
||||
import {
|
||||
hasComplaintVerification,
|
||||
isComplaintAuthContext,
|
||||
} from "@/complaints/utils/complaintVerificationStorage";
|
||||
import { COMPLAINT_RECORDS_PATH } from "@/complaints/utils/complaintRoutes";
|
||||
import { useExternalPortalSession } from "@/shared/hooks/useExternalPortalSession";
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
path?: string;
|
||||
requiresCompleteRegistration?: true;
|
||||
children?: NavItem[];
|
||||
}
|
||||
|
||||
const languageOptions = UI_LANGUAGE_OPTIONS;
|
||||
|
||||
const Header = () => {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [servicesOpen, setServicesOpen] = useState(false);
|
||||
const [navigateTo, setNaviageTo] = useState("/");
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isComplaintContext = isComplaintAuthContext(location.pathname);
|
||||
const { isAuthenticated, showExternalPortalChrome } = useExternalPortalSession();
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const { config: tenantConfig } = useTenantConfig();
|
||||
const moduleConfig = resolveModuleConfig(tenantConfig);
|
||||
|
||||
const userDetails = useUser();
|
||||
const { t, i18n } = useTranslation();
|
||||
const currentLanguage = resolveUiLanguage(i18n.language);
|
||||
|
||||
const hasCompletedRegistration = userDetails?.hasFinishedRegistration;
|
||||
const changeLanguage = (lng: string) => i18n.changeLanguage(lng);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem("i18nextLng")) {
|
||||
i18n.changeLanguage("am");
|
||||
localStorage.setItem("i18nextLng", "am");
|
||||
}
|
||||
}, [i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 10);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const handleSignIn = () => {
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname.includes("/external-portal/portal-outgoing")) {
|
||||
setActiveTab("records");
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname.startsWith("/complaints")) {
|
||||
setActiveTab("complaint");
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const navItems: NavItem[] = useMemo(() => {
|
||||
if (showExternalPortalChrome) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return [
|
||||
{ id: "overview", label: t("landingPage.overview") },
|
||||
{ id: "workflow", label: t("landingPage.workflow") },
|
||||
];
|
||||
}
|
||||
|
||||
const complaintServices: NavItem[] = moduleConfig.complaint
|
||||
? [
|
||||
{
|
||||
id: "complaint",
|
||||
label: t("landingPage.fileComplaint"),
|
||||
path: "/complaints",
|
||||
},
|
||||
{
|
||||
id: "follow-complaint",
|
||||
label: t("landingPage.followComplaint"),
|
||||
path: "/follow-complaint",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const items: NavItem[] = [
|
||||
{ id: "overview", label: t("landingPage.overview") },
|
||||
{ id: "features", label: t("landingPage.features") },
|
||||
];
|
||||
|
||||
if (complaintServices.length > 0) {
|
||||
items.push({
|
||||
id: "services",
|
||||
label: t("landingPage.services"),
|
||||
children: complaintServices,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
{ id: "workflow", label: t("landingPage.workflow") },
|
||||
{ id: "howto", label: t("landingPage.manual") },
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [isAuthenticated, moduleConfig.complaint, showExternalPortalChrome, t]);
|
||||
|
||||
const handleNavClick = (item: NavItem) => {
|
||||
if (
|
||||
item.requiresCompleteRegistration &&
|
||||
!hasCompletedRegistration &&
|
||||
!hasComplaintVerification()
|
||||
) {
|
||||
toast.error(t("registration.registrationRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.path) {
|
||||
navigate(item.path);
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Animation variants
|
||||
const mobileMenuVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
when: "afterChildren",
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
height: "auto",
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
when: "beforeChildren",
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mobileItemVariants = {
|
||||
hidden: { x: -20, opacity: 0 },
|
||||
visible: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
x: { stiffness: 1000, velocity: -100 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const servicesVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: -10,
|
||||
scale: 0.95,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const serviceItemVariants = {
|
||||
hidden: { opacity: 0, y: -5 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
const getServiceIcon = (id: string) => {
|
||||
switch (id) {
|
||||
case "complaint":
|
||||
return <FileText className="w-4 h-4" />;
|
||||
case "follow-complaint":
|
||||
return <Search className="w-4 h-4" />;
|
||||
case "about-us":
|
||||
return <Info className="w-4 h-4" />;
|
||||
default:
|
||||
return <FileText className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Get current language display name
|
||||
const getCurrentLanguageDisplay = () => {
|
||||
const currentLang = languageOptions.find(
|
||||
(lang) => lang.value === currentLanguage,
|
||||
);
|
||||
return currentLang ? currentLang.label : "English";
|
||||
};
|
||||
useMemo(() => {
|
||||
if (showExternalPortalChrome) {
|
||||
setNaviageTo(COMPLAINT_RECORDS_PATH);
|
||||
}
|
||||
}, [showExternalPortalChrome]);
|
||||
return (
|
||||
<>
|
||||
{/* Registration Alert - Fixed for mobile */}
|
||||
<nav
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-white dark:bg-gray-900 shadow-lg"
|
||||
: "bg-white dark:bg-gray-900 shadow-sm"
|
||||
} `}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center gap-4 h-16 md:h-20">
|
||||
{/* Logo Section */}
|
||||
<div className="flex items-center">
|
||||
{!showExternalPortalChrome ? (
|
||||
<>
|
||||
{tenantConfig?.logo ? (
|
||||
<div className="relative group inline-flex">
|
||||
{/* Glow */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 group-hover:opacity-100 transition-all duration-500 scale-110" />
|
||||
|
||||
{/* Footer Logo Wrapper */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.04 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
transition={{ type: "spring", stiffness: 220, damping: 16 }}
|
||||
className="relative inline-flex items-center justify-center cursor-pointer group"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
showExternalPortalChrome
|
||||
? COMPLAINT_RECORDS_PATH
|
||||
: isComplaintContext
|
||||
? "/complaints"
|
||||
: "/",
|
||||
)
|
||||
}
|
||||
title={t("nav.homePage")}
|
||||
>
|
||||
<motion.img
|
||||
src={tenantConfig.logo}
|
||||
alt="Footer Logo"
|
||||
className="
|
||||
h-15 md:h-30
|
||||
w-auto
|
||||
object-contain
|
||||
group-hover:scale-105
|
||||
transition-transform
|
||||
duration-300
|
||||
"
|
||||
onError={(e) => {
|
||||
const fallback =
|
||||
e.currentTarget.parentElement?.querySelector(
|
||||
".footer-logo-fallback",
|
||||
);
|
||||
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden");
|
||||
}
|
||||
|
||||
e.currentTarget.remove();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Fallback when logo fails to load */}
|
||||
<div
|
||||
className="
|
||||
footer-logo-fallback
|
||||
hidden
|
||||
h-20
|
||||
w-20
|
||||
items-center
|
||||
justify-center
|
||||
rounded-full
|
||||
bg-primary
|
||||
text-primary-foreground
|
||||
font-bold
|
||||
text-2xl
|
||||
pointer-events-none
|
||||
"
|
||||
>
|
||||
{tenantConfig?.organizationName
|
||||
?.split(/[\s\-–—]+/)
|
||||
.filter(Boolean)
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase() || "SO"}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="h-10 w-10 md:h-12 md:w-12 rounded-full bg-primary flex items-center justify-center text-white font-bold cursor-pointer"
|
||||
onClick={() => navigate(navigateTo)}
|
||||
title={t("nav.homePage")}
|
||||
>
|
||||
SO
|
||||
</motion.div>
|
||||
)}
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="ml-2 md:ml-3 text-lg md:text-xl font-bold text-gray-800 dark:text-white cursor-pointer"
|
||||
onClick={() => navigate(navigateTo)}
|
||||
title={t("nav.homePage")}
|
||||
>
|
||||
{tenantConfig?.organizationName ||
|
||||
tenantConfig?.organizationName == ""
|
||||
? tenantConfig?.organizationName
|
||||
: t("landingPage.smartOffice")}
|
||||
</motion.span>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:ml-6 lg:ml-10 md:flex md:space-x-4 lg:space-x-6">
|
||||
{navItems.map((item) => (
|
||||
<div key={item.id} className="relative">
|
||||
{item.children ? (
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setServicesOpen(true)}
|
||||
onMouseLeave={() => setServicesOpen(false)}
|
||||
>
|
||||
<button
|
||||
onClick={() => setServicesOpen(!servicesOpen)}
|
||||
className={`relative inline-flex items-center px-1 pt-1 text-sm lg:text-base font-medium transition-colors duration-200 cursor-pointer ${
|
||||
activeTab === item.id
|
||||
? "text-gray-900 dark:text-white"
|
||||
: "text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white hover:underline"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
<ChevronDown
|
||||
className={`ml-1 w-4 h-4 transition-transform ${
|
||||
servicesOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{servicesOpen && (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
variants={servicesVariants}
|
||||
className="absolute top-full left-0 mt-2 w-64 bg-white rounded-lg shadow-xl border border-gray-200 z-50 overflow-hidden"
|
||||
>
|
||||
<div className="py-2">
|
||||
{item.children.map((child) => (
|
||||
<motion.button
|
||||
key={child.id}
|
||||
variants={serviceItemVariants}
|
||||
onClick={() => handleNavClick(child)}
|
||||
className="w-full flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition-colors duration-200"
|
||||
>
|
||||
<span className="mr-3 text-primary">
|
||||
{getServiceIcon(child.id)}
|
||||
</span>
|
||||
{child.label}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab(item.id);
|
||||
if (item.path) {
|
||||
handleNavClick(item);
|
||||
} else {
|
||||
const el = document.getElementById(item.id);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`relative inline-flex items-center px-1 pt-1 text-sm lg:text-base font-medium transition-colors duration-200 cursor-pointer ${
|
||||
(item.path
|
||||
? location.pathname.startsWith(item.path)
|
||||
: activeTab === item.id)
|
||||
? "text-gray-900 dark:text-white"
|
||||
: "text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white hover:underline"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
{(item.path
|
||||
? location.pathname.startsWith(item.path)
|
||||
: activeTab === item.id) && (
|
||||
<motion.div
|
||||
layoutId="activeTabIndicator"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
|
||||
transition={{
|
||||
type: "spring",
|
||||
bounce: 0.2,
|
||||
duration: 0.6,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Sign Up Button */}
|
||||
{!isAuthenticated && (
|
||||
<div className="hidden md:flex items-center space-x-2 lg:space-x-4">
|
||||
<div className="w-28 lg:w-32">
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={(lng) => changeLanguage(lng)}
|
||||
>
|
||||
<SelectTrigger className="w-full text-sm rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 px-2 py-1.5 lg:px-3 lg:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<SelectValue>{getCurrentLanguageDisplay()}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dark:bg-gray-800 dark:border-gray-700">
|
||||
{languageOptions.map((lang) => (
|
||||
<SelectItem
|
||||
key={lang.value}
|
||||
value={lang.value}
|
||||
className="dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
{lang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Dark Mode Toggle */}
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200"
|
||||
title={
|
||||
isDarkMode ? "Switch to light mode" : "Switch to dark mode"
|
||||
}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-5 w-5 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5 text-gray-600" />
|
||||
)}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={handleSignIn}
|
||||
whileHover={{
|
||||
scale: 1.03,
|
||||
boxShadow: "0 4px 12px rgba(24, 170, 157, 0.2)",
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="inline-flex items-center px-3 py-1.5 lg:px-4 lg:py-2.5 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all duration-200 whitespace-nowrap"
|
||||
>
|
||||
{t("landingPage.signIn")}
|
||||
</motion.button>
|
||||
</div>
|
||||
)}
|
||||
{showExternalPortalChrome && (
|
||||
<div className="hidden md:flex items-center">
|
||||
<ExternalPortal />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="flex items-center md:hidden">
|
||||
<motion.button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="inline-flex items-center justify-center p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary transition-colors duration-200"
|
||||
aria-expanded="false"
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
) : (
|
||||
<Menu className="h-6 w-6" />
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
variants={mobileMenuVariants}
|
||||
className="md:hidden overflow-hidden bg-white dark:bg-gray-800 shadow-xl border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<motion.div className="pt-2 pb-4 space-y-1 px-4">
|
||||
{navItems.map((item) => (
|
||||
<div key={item.id}>
|
||||
{item.children ? (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setServicesOpen(!servicesOpen)}
|
||||
className={`block w-full text-left pl-3 pr-4 py-3 border-l-4 text-base font-medium transition-all duration-200 ${
|
||||
activeTab === item.id
|
||||
? "bg-primary-500/15 border-primary text-primary"
|
||||
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{item.label}
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 transition-transform ${
|
||||
servicesOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{servicesOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="pl-6 overflow-hidden"
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<motion.button
|
||||
key={child.id}
|
||||
onClick={() => handleNavClick(child)}
|
||||
className="block w-full text-left pl-3 pr-4 py-2 text-sm text-gray-600 hover:bg-blue-50 hover:text-blue-700 transition-colors duration-200"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-primary">
|
||||
{getServiceIcon(child.id)}
|
||||
</span>
|
||||
{child.label}
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : (
|
||||
<motion.button
|
||||
variants={mobileItemVariants}
|
||||
onClick={() => {
|
||||
setActiveTab(item.id);
|
||||
setMobileMenuOpen(false);
|
||||
if (item.path) {
|
||||
handleNavClick(item);
|
||||
}
|
||||
}}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className={`block w-full text-left pl-3 pr-4 py-3 border-l-4 text-base font-medium transition-all duration-200 ${
|
||||
activeTab === item.id
|
||||
? "bg-primary-500/15 border-primary text-primary"
|
||||
: "border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 hover:text-gray-800 dark:hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!isAuthenticated && (
|
||||
<motion.div
|
||||
variants={mobileItemVariants}
|
||||
className="pt-4 pb-2 border-t border-gray-200 dark:border-gray-700 space-y-3"
|
||||
>
|
||||
<div className="px-2">
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={(lng) => changeLanguage(lng)}
|
||||
>
|
||||
<SelectTrigger className="w-full text-sm bg-gray-100 dark:bg-gray-700 border-none rounded-md px-3 py-2.5 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary dark:text-gray-200">
|
||||
<SelectValue>
|
||||
{getCurrentLanguageDisplay()}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dark:bg-gray-800 dark:border-gray-700">
|
||||
{languageOptions.map((lang) => (
|
||||
<SelectItem
|
||||
key={lang.value}
|
||||
value={lang.value}
|
||||
className="dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
{lang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Dark Mode Toggle for Mobile */}
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="block w-full px-4 py-3 text-base font-medium text-center text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-lg shadow hover:bg-gray-200 dark:hover:bg-gray-600 transition-all duration-200 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<>
|
||||
<Sun className="h-5 w-5 text-yellow-500" />
|
||||
{t("landingPage.lightMode")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Moon className="h-5 w-5 text-gray-600" />
|
||||
{t("landingPage.darkMode")}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
handleSignIn();
|
||||
setMobileMenuOpen(false);
|
||||
}}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="block w-full px-4 py-3 text-base font-medium text-center text-white bg-primary rounded-lg shadow hover:bg-primary-700 transition-all duration-200"
|
||||
>
|
||||
{t("landingPage.signIn")}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{showExternalPortalChrome && (
|
||||
<motion.div
|
||||
variants={mobileItemVariants}
|
||||
className="pt-3 border-t border-gray-200"
|
||||
>
|
||||
<ExternalPortal
|
||||
mobileView={true}
|
||||
onItemClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,560 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, useAnimation, Variants } from "framer-motion";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { easeInOut } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Cookies from "js-cookie";
|
||||
import { ArrowRight, BookOpenText, ShieldCheck } from "lucide-react";
|
||||
import {
|
||||
useTenantConfig,
|
||||
resolveModuleConfig,
|
||||
} from "@/layout/components/TenantConfig";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/common/ui/dialog";
|
||||
|
||||
const HeroSection = () => {
|
||||
const controls = useAnimation();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { config: tenantConfig } = useTenantConfig();
|
||||
const moduleConfig = resolveModuleConfig(tenantConfig);
|
||||
const primary = tenantConfig.primaryColor || "#18AA9D";
|
||||
const secondary = tenantConfig.secondaryColor || primary;
|
||||
const [ref, inView] = useInView({
|
||||
threshold: 0.1,
|
||||
triggerOnce: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inView) {
|
||||
controls.start("visible");
|
||||
} else {
|
||||
controls.start("hidden");
|
||||
}
|
||||
}, [controls, inView]);
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.15,
|
||||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants: Variants = {
|
||||
hidden: { y: 30, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: easeInOut,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const isTokenPresent = Boolean(Cookies.get("auth-token"));
|
||||
const heroTitle =
|
||||
tenantConfig?.organizationName || t("landingPage.smartOffice");
|
||||
const welcomeMessage =
|
||||
tenantConfig?.welcomeMessage || t("landingPage.initiativeDescription");
|
||||
const organizationInitials =
|
||||
heroTitle
|
||||
?.split(/[\s\-–—]+/)
|
||||
.filter(Boolean)
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase() || "SO";
|
||||
|
||||
const cardVariants: Variants = {
|
||||
hidden: { scale: 0.85, opacity: 0, rotateX: 10 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
rotateX: 0,
|
||||
transition: {
|
||||
delay: 0.4,
|
||||
duration: 0.8,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const floatingElementVariants: Variants = {
|
||||
float: {
|
||||
y: [0, -20, 0],
|
||||
transition: {
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut", // ✅ correct literal type
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative bg-gradient-to-br from-white via-blue-50/30 to-primary-50/50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 overflow-x-hidden min-h-screen flex items-start pt-8">
|
||||
{/* Enhanced Background */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{/* Gradient Orbs */}
|
||||
<div className="absolute -top-40 -left-40 w-80 h-80 bg-gradient-to-r from-primary to-primary-500 rounded-full mix-blend-multiply filter blur-3xl opacity-15 animate-orb-slow dark:opacity-10"></div>
|
||||
<div className="absolute -top-20 -right-20 w-96 h-96 bg-gradient-to-r from-primary-500 to-primary rounded-full mix-blend-multiply filter blur-3xl opacity-10 animate-orb-medium dark:opacity-5"></div>
|
||||
<div className="absolute -bottom-40 left-1/3 w-72 h-72 bg-gradient-to-r from-primary-700 to-primary-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-orb-fast dark:opacity-10"></div>
|
||||
|
||||
{/* Grid Pattern */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(24,170,157,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(24,170,157,0.03)_1px,transparent_1px)] bg-[size:60px_60px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_50%,black,transparent)] dark:bg-[linear-gradient(rgba(24,170,157,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(24,170,157,0.05)_1px,transparent_1px)]"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 pt-12 lg:pt-20">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-20 items-start">
|
||||
{/* Enhanced Content */}
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={controls}
|
||||
variants={containerVariants}
|
||||
className="space-y-8 lg:space-y-10"
|
||||
>
|
||||
{!isTokenPresent && (
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-primary/10 to-primary-500/10 border border-primary/20 text-primary text-sm font-medium mb-4"
|
||||
>
|
||||
<span className="relative flex h-2 w-2 mr-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
||||
</span>
|
||||
{"Online"}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.h1
|
||||
variants={itemVariants}
|
||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-gray-900 dark:text-white leading-tight"
|
||||
>
|
||||
<span className="block bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent">
|
||||
{tenantConfig?.organizationName || t("landingPage.smartOffice")}
|
||||
</span>
|
||||
<span className="block text-transparent bg-clip-text bg-gradient-to-r from-primary via-primary-500 to-primary-500">
|
||||
{t("landingPage.platformSystem")}
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={itemVariants}
|
||||
className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-lg leading-relaxed"
|
||||
>
|
||||
{welcomeMessage}
|
||||
</motion.p>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button className="group inline-flex items-center gap-3 rounded-full border border-primary/25 bg-white/75 px-5 py-3 text-sm font-semibold text-primary shadow-sm shadow-primary/10 backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/45 hover:bg-primary/10 hover:shadow-lg hover:shadow-primary/15 focus:outline-none focus:ring-2 focus:ring-primary/40 focus:ring-offset-2 dark:bg-gray-900/60 dark:hover:bg-primary/15 dark:focus:ring-offset-gray-900">
|
||||
<span className="flex size-9 items-center justify-center rounded-full bg-primary/10 text-primary transition-colors duration-300 group-hover:bg-primary group-hover:text-white">
|
||||
<BookOpenText className="size-4" />
|
||||
</span>
|
||||
<span>{t("newssection.readMore")}</span>
|
||||
<ArrowRight className="size-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[86vh] max-w-3xl overflow-hidden rounded-2xl border border-primary/15 bg-white/95 p-0 shadow-2xl shadow-primary/10 backdrop-blur-xl dark:border-primary/20 dark:bg-gray-900/95">
|
||||
<div className="h-1.5 bg-gradient-to-r from-primary via-primary-500 to-primary" />
|
||||
<DialogHeader className="relative gap-0 px-6 pb-5 pt-6 text-left sm:px-8">
|
||||
<div className="absolute right-8 top-8 hidden h-24 w-24 rounded-full bg-primary/10 blur-2xl sm:block" />
|
||||
<div className="relative flex items-start gap-4">
|
||||
{tenantConfig?.logo ? (
|
||||
<div className="relative group inline-flex shrink-0">
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 transition-all duration-500 scale-110 group-hover:opacity-100" />
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.04 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 220,
|
||||
damping: 16,
|
||||
}}
|
||||
className="relative flex h-16 w-24 items-center justify-center overflow-hidden rounded-2xl border-none bg-white/5 shadow-md dark:bg-slate-900/40"
|
||||
title={heroTitle}
|
||||
>
|
||||
<motion.img
|
||||
src={tenantConfig.logo}
|
||||
alt={`${heroTitle} logo`}
|
||||
className="h-full w-full object-contain drop-shadow-lg transition-transform duration-300 group-hover:scale-105"
|
||||
onError={(e) => {
|
||||
const fallback =
|
||||
e.currentTarget.parentElement?.querySelector(
|
||||
".modal-logo-fallback",
|
||||
);
|
||||
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden");
|
||||
fallback.classList.add("flex");
|
||||
}
|
||||
|
||||
e.currentTarget.remove();
|
||||
}}
|
||||
/>
|
||||
<div className="modal-logo-fallback absolute inset-0 hidden items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-primary-500 text-xl font-bold text-white">
|
||||
{organizationInitials || "SO"}
|
||||
</div>
|
||||
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/15 to-transparent transition-transform duration-1000 group-hover:translate-x-full" />
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative shrink-0">
|
||||
<div className="absolute inset-0 rounded-2xl bg-primary/20 blur-xl scale-110" />
|
||||
<div className="relative flex h-16 w-24 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-br from-primary to-primary-500 text-xl font-bold text-white shadow-lg shadow-primary/25 ring-1 ring-white/20">
|
||||
<span className="drop-shadow-sm">
|
||||
{organizationInitials || "SO"}
|
||||
</span>
|
||||
<div className="absolute inset-x-0 top-0 h-1/2 bg-white/15" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 pt-1">
|
||||
<DialogTitle className="text-2xl font-bold leading-tight text-gray-950 dark:text-white sm:text-3xl">
|
||||
{heroTitle}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{welcomeMessage}
|
||||
</DialogDescription>
|
||||
<div className="mt-3 h-1 w-20 rounded-full bg-gradient-to-r from-primary to-primary-500" />
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="border-t border-gray-100 bg-gradient-to-b from-gray-50/80 to-white px-6 py-6 dark:border-gray-800 dark:from-gray-950/40 dark:to-gray-900 sm:px-8">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 text-base leading-8 text-gray-700 shadow-sm dark:border-gray-800 dark:bg-gray-900/80 dark:text-gray-300 sm:p-6 sm:text-lg">
|
||||
<p className="whitespace-pre-line">{welcomeMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col sm:flex-row flex-wrap gap-4 pt-2"
|
||||
>
|
||||
{moduleConfig.complaint && (
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
boxShadow: `0 20px 40px ${primary}4d`,
|
||||
}}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/complaints")}
|
||||
className="group relative px-8 py-4 rounded-xl bg-gradient-to-r from-primary to-primary-500 text-white font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span className="relative flex items-center justify-center">
|
||||
{t("complaint.fayda.submitComplaint")}
|
||||
<ShieldCheck className="w-5 h-5 ml-2" />
|
||||
</span>
|
||||
</motion.button>
|
||||
)}
|
||||
{!isTokenPresent && (
|
||||
<motion.a
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
boxShadow: `0 20px 40px ${primary}4d`,
|
||||
}}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
href="/login"
|
||||
className="group relative px-8 py-4 rounded-xl bg-gradient-to-r from-primary to-primary-500 text-white font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span className="relative flex items-center justify-center">
|
||||
{t("landingPage.signIn")}
|
||||
<svg
|
||||
className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</motion.a>
|
||||
)}
|
||||
<motion.a
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
backgroundColor: "rgba(24, 170, 157, 0.08)",
|
||||
}}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
href="#features"
|
||||
className="group px-8 py-4 rounded-xl border-2 border-primary text-primary font-semibold text-lg hover:shadow-lg transition-all duration-300 flex items-center justify-center"
|
||||
>
|
||||
{t("landingPage.learnMore")}
|
||||
<svg
|
||||
className="ml-2 w-4 h-4 group-hover:translate-y-0.5 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
||||
/>
|
||||
</svg>
|
||||
</motion.a>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-wrap gap-8 pt-5 pb-10"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
95%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{t("landingPage.uptime")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
25+
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{t("landingPage.bureaus")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
24/7
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{t("landingPage.support")}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Enhanced Card */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={controls}
|
||||
variants={cardVariants}
|
||||
className="relative"
|
||||
>
|
||||
<div className="absolute -inset-4 bg-gradient-to-r from-primary to-primary-500 rounded-3xl opacity-20 blur-xl animate-pulse-slow"></div>
|
||||
<div className="absolute -inset-2 bg-gradient-to-r from-primary to-primary-500 rounded-2xl opacity-10 blur-lg"></div>
|
||||
|
||||
<motion.div
|
||||
whileHover={{ y: -5, rotateX: 5 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||
className="relative h-full bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-primary to-primary-500"></div>
|
||||
|
||||
<div className="p-8 lg:p-10 h-full flex flex-col items-center justify-center bg-gradient-to-br from-white/90 to-gray-50/80 dark:from-gray-800/90 dark:to-gray-700/80">
|
||||
<div className="text-center space-y-6">
|
||||
{/* Logo/Brand */}
|
||||
<div className="mb-6">
|
||||
<div
|
||||
className="w-16 h-16 mx-auto mb-4 rounded-2xl shadow-lg flex items-center justify-center"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${primary}, ${secondary})`,
|
||||
}}
|
||||
>
|
||||
<span className="text-white font-bold text-xl">SO</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-4xl lg:text-5xl font-bold mb-2 tracking-tight text-slate-900 dark:text-white">
|
||||
{t("landingPage.sops")}
|
||||
</div>
|
||||
|
||||
<div className="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||
{tenantConfig.appName}
|
||||
</div>
|
||||
|
||||
<div className="text-gray-600 dark:text-gray-400 max-w-md leading-relaxed">
|
||||
{t("landingPage.revolutionDescription")}
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<div className="grid grid-cols-2 gap-4 pt-4">
|
||||
{[
|
||||
"features1",
|
||||
"features2",
|
||||
"features3",
|
||||
"features4",
|
||||
"features5",
|
||||
"features6",
|
||||
].map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.8 + index * 0.1 }}
|
||||
className="flex items-center text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full mr-2"
|
||||
style={{ backgroundColor: primary }}
|
||||
></div>
|
||||
{t(`landingPage.${feature}`)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="inline-flex items-center px-6 py-3 rounded-full font-semibold backdrop-blur-sm"
|
||||
style={{
|
||||
background: `linear-gradient(to right, ${primary}1a, ${secondary}1a)`,
|
||||
color: primary,
|
||||
border: `1px solid ${primary}33`,
|
||||
}}
|
||||
>
|
||||
<span className="relative flex h-3 w-3 mr-3">
|
||||
<span
|
||||
className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
|
||||
style={{ backgroundColor: primary }}
|
||||
></span>
|
||||
<span
|
||||
className="relative inline-flex rounded-full h-3 w-3"
|
||||
style={{ backgroundColor: primary }}
|
||||
></span>
|
||||
</span>
|
||||
{t("landingPage.liveDemo")}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Floating Elements */}
|
||||
<motion.div
|
||||
variants={floatingElementVariants}
|
||||
animate="float"
|
||||
className="hidden lg:block absolute bottom-20 left-20"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary/20 to-primary-500/20 border border-primary/10 backdrop-blur-sm rotate-45"></div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={floatingElementVariants}
|
||||
animate="float"
|
||||
transition={{ delay: 1 }}
|
||||
className="hidden lg:block absolute top-32 right-32"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-primary-500/15 to-primary/15 border border-primary-500/10 backdrop-blur-sm"></div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={floatingElementVariants}
|
||||
animate="float"
|
||||
transition={{ delay: 2 }}
|
||||
className="hidden lg:block absolute top-1/2 left-1/4"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-700/10 to-primary-500/10 border border-primary-700/10 backdrop-blur-sm rotate-12"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 2 }}
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 hidden lg:block"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, 10, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="w-6 h-10 border-2 border-gray-300 rounded-full flex justify-center"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, 12, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="w-1 h-3 bg-gray-400 rounded-full mt-2"
|
||||
></motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
@keyframes orb-slow {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(40px, -60px) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) scale(0.9);
|
||||
}
|
||||
}
|
||||
@keyframes orb-medium {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(-50px, 40px) scale(1.05);
|
||||
}
|
||||
66% {
|
||||
transform: translate(20px, -20px) scale(0.95);
|
||||
}
|
||||
}
|
||||
@keyframes orb-fast {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(20px, -40px) scale(1.08);
|
||||
}
|
||||
}
|
||||
.animate-orb-slow {
|
||||
animation: orb-slow 15s infinite ease-in-out;
|
||||
}
|
||||
.animate-orb-medium {
|
||||
animation: orb-medium 12s infinite ease-in-out;
|
||||
}
|
||||
.animate-orb-fast {
|
||||
animation: orb-fast 10s infinite ease-in-out;
|
||||
}
|
||||
@keyframes pulse-slow {
|
||||
0%, 100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
.animate-pulse-slow {
|
||||
animation: pulse-slow 4s infinite ease-in-out;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSection;
|
||||
609
apps/edr-freight-web/backoffice/src/layout/components/Howto.tsx
Normal file
609
apps/edr-freight-web/backoffice/src/layout/components/Howto.tsx
Normal file
@@ -0,0 +1,609 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const RecordManagementGuide = () => {
|
||||
const [activeTab, setActiveTab] = useState("dashboard");
|
||||
const { t } = useTranslation();
|
||||
const [expandedStep, setExpandedStep] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
setExpandedStep(null);
|
||||
//window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const toggleStep = (stepId: string) => {
|
||||
setExpandedStep(expandedStep === stepId ? null : stepId);
|
||||
};
|
||||
const [openStatus, setOpenStatus] = useState<string | null>(null);
|
||||
|
||||
const toggle = (status: string) => {
|
||||
setOpenStatus((prev) => (prev === status ? null : status));
|
||||
};
|
||||
const steps = [
|
||||
{
|
||||
id: "login",
|
||||
title: t("landingPage.login"),
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="dark:text-gray-300">
|
||||
{t("landingPage.step1")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{t("landingPage.signIn")}
|
||||
</span>{" "}
|
||||
{t("landingPage.headerBtn")}
|
||||
</p>
|
||||
<p className="dark:text-gray-300">{t("landingPage.step2")}</p>
|
||||
<p className="dark:text-gray-300">
|
||||
{t("landingPage.step3")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{t("auth.login")}
|
||||
</span>{" "}
|
||||
{t("landingPage.accessBtn")}
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 mt-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<span className="font-semibold">{t("landingPage.note")}</span>{" "}
|
||||
{t("landingPage.forgotIntro")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/login-screen.png",
|
||||
},
|
||||
{
|
||||
id: "dashboard",
|
||||
title: t("landingPage.dashboardTitle"),
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="dark:text-gray-300">{t("landingPage.dashboardDesc")}</p>
|
||||
<ul className="list-disc pl-5 space-y-2 dark:text-gray-300">
|
||||
<li>
|
||||
<span className="font-semibold">
|
||||
{t("landingPage.recordsCreated")}:
|
||||
</span>{" "}
|
||||
{t("landingPage.recordsTotal")}
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold">
|
||||
{t("landingPage.recordsReceived")}:
|
||||
</span>{" "}
|
||||
{t("landingPage.incomingDocs")}
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold">
|
||||
{t("landingPage.breakdown")}:
|
||||
</span>{" "}
|
||||
{t("landingPage.visuals")}
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold">
|
||||
{t("landingPage.approvalStatus")}:
|
||||
</span>{" "}
|
||||
{t("landingPage.approvalTypes")}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">
|
||||
{t("landingPage.quickActions")}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||
{t("landingPage.createOrCheck")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">
|
||||
{t("landingPage.recentActivity")}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||
{t("landingPage.trackUpdates")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/dashboard-screen.png",
|
||||
},
|
||||
{
|
||||
id: "outgoing",
|
||||
title: t("landingPage.manageOut"),
|
||||
content: (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{t("landingPage.createNew")}
|
||||
</h4>
|
||||
<ol className="list-decimal pl-5 space-y-2 mt-2 dark:text-gray-300">
|
||||
<li>
|
||||
{t("landingPage.goToTab")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{t("statusBar.Outgoing")}
|
||||
</span>{" "}
|
||||
{t("landingPage.tab")}
|
||||
</li>
|
||||
<li>
|
||||
{t("landingPage.click")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{t("userRecord.Add Record")}
|
||||
</span>{" "}
|
||||
{t("landingPage.button")}
|
||||
</li>
|
||||
<li>{t("landingPage.fillFields")}</li>
|
||||
<li>{t("landingPage.attach")}</li>
|
||||
<li>{t("landingPage.saveOrSubmit")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{t("landingPage.trackStatus")}
|
||||
</h4>
|
||||
<div className="mt-2 grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{
|
||||
status: "Draft",
|
||||
color: "bg-gray-200 dark:bg-gray-700",
|
||||
detail: t("landingPage.notForwarded"),
|
||||
},
|
||||
{
|
||||
status: "Submitted",
|
||||
color: "bg-purple-100 dark:bg-purple-900/50",
|
||||
detail: t("landingPage.awaiting"),
|
||||
},
|
||||
{
|
||||
status: "Accepted",
|
||||
color: "bg-primary-100 dark:bg-primary-900/50",
|
||||
detail: t("landingPage.accepted"),
|
||||
},
|
||||
{
|
||||
status: "Approved",
|
||||
color: "bg-primary-200 dark:bg-primary-800/50",
|
||||
detail: t("landingPage.approvedSent"),
|
||||
},
|
||||
{
|
||||
status: "Adjustment",
|
||||
color: "bg-yellow-100 dark:bg-yellow-900/50",
|
||||
detail: t("landingPage.returned"),
|
||||
},
|
||||
{
|
||||
status: "Rejected",
|
||||
color: "bg-red-100 dark:bg-red-900/50",
|
||||
detail: t("landingPage.rejected"),
|
||||
},
|
||||
{
|
||||
status: "Sent",
|
||||
color: "bg-primary-300 dark:bg-primary-700",
|
||||
detail: t("landingPage.sent"),
|
||||
},
|
||||
{
|
||||
status: "Returned",
|
||||
color: "bg-gray-400 dark:bg-gray-600",
|
||||
detail: t("landingPage.returnedByOfficer"),
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
className={`${item.color} p-2 rounded text-center text-sm font-medium relative`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="mx-auto dark:text-gray-900">
|
||||
{t(`statusBar.${item.status}`)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => toggle(item.status)}
|
||||
className="ml-2 text-xs text-primary hover:underline"
|
||||
>
|
||||
<span
|
||||
className={`inline-block transition-transform duration-200 ${
|
||||
openStatus === item.status ? "rotate-180" : ""
|
||||
}`}
|
||||
>
|
||||
▼
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{openStatus === item.status && (
|
||||
<div className="mt-2 p-2 text-xs bg-white dark:bg-gray-700 border dark:border-gray-600 rounded shadow absolute top-full left-0 w-full z-10 dark:text-gray-200">
|
||||
{item.detail}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-3">
|
||||
{t("landingPage.monitor")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/outgoing-screen.png",
|
||||
},
|
||||
{
|
||||
id: "incoming",
|
||||
title: t("landingPage.manageIncoming"),
|
||||
content: (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{t("landingPage.incomingTypes")}
|
||||
</h4>
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{[
|
||||
{ type: "external", desc: t("landingPage.fromOthers") },
|
||||
{ type: "internal", desc: t("landingPage.withinOrg") },
|
||||
{ type: "cc", desc: t("landingPage.copied") },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.type}
|
||||
className="bg-white dark:bg-gray-800 p-3 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||
>
|
||||
<h5 className="font-medium text-primary">
|
||||
{t(`nav.${item.type}`)}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{t("landingPage.processIncoming")}
|
||||
</h4>
|
||||
<ul className="list-disc pl-5 space-y-2 mt-2">
|
||||
<li className="dark:text-gray-300">
|
||||
<span className="font-semibold">{t("userRecord.View")}:</span>{" "}
|
||||
{t("landingPage.readDoc")}
|
||||
</li>
|
||||
<li className="dark:text-gray-300">
|
||||
<span className="font-semibold">{t("statusBar.Accept")}:</span>{" "}
|
||||
{t("landingPage.acknowledge")}
|
||||
</li>
|
||||
<li className="dark:text-gray-300">
|
||||
<span className="font-semibold">{t("statusBar.Assign")}:</span>{" "}
|
||||
{t("landingPage.forward")}
|
||||
</li>
|
||||
<li className="dark:text-gray-300">
|
||||
<span className="font-semibold">
|
||||
{t("landingPage.archive")}:
|
||||
</span>{" "}
|
||||
{t("landingPage.fileRef")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/incoming-screen.png",
|
||||
},
|
||||
{
|
||||
id: "approval",
|
||||
title: t("landingPage.approvalworkflow"),
|
||||
content: (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{t("landingPage.process")}
|
||||
</h4>
|
||||
<div className="mt-4">
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t("landingPage.internalFlow")}
|
||||
</h5>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
{[
|
||||
t("landingPage.creator"),
|
||||
t("landingPage.leader"),
|
||||
t("landingPage.director"),
|
||||
t("landingPage.officer"),
|
||||
].map((role, i) => (
|
||||
<div key={i} className="flex flex-col items-center">
|
||||
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-white font-medium text-sm">
|
||||
{i + 1}
|
||||
</div>
|
||||
<span className="text-xs mt-1 text-center dark:text-gray-300">{role}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t("landingPage.externalFlow")}
|
||||
</h5>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
{[
|
||||
t("landingPage.creator"),
|
||||
t("landingPage.leader"),
|
||||
t("landingPage.director"),
|
||||
t("landingPage.officer"),
|
||||
].map((role, i) => (
|
||||
<div key={i} className="flex flex-col items-center">
|
||||
<div className="h-10 w-10 rounded-full bg-primary-500 flex items-center justify-center text-white font-medium text-sm">
|
||||
{i + 1}
|
||||
</div>
|
||||
<span className="text-xs mt-1 text-center dark:text-gray-300">{role}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{t("landingPage.actions")}
|
||||
</h4>
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{[
|
||||
{
|
||||
action: "Approve",
|
||||
desc:t("landingPage.addTeeterAndSignature"),
|
||||
color: "bg-primary-50 dark:bg-primary-900/30 border-primary-200 dark:border-primary-800",
|
||||
},
|
||||
{
|
||||
action: "Reject",
|
||||
desc: t("landingPage.returnWithComments"),
|
||||
color: "bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800",
|
||||
},
|
||||
{
|
||||
action: "Adjust",
|
||||
desc: t("landingPage.requestModifications"),
|
||||
color: "bg-yellow-50 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800",
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.action}
|
||||
className={`${item.color} p-3 rounded-lg border`}
|
||||
>
|
||||
<h5 className="font-medium dark:text-gray-200">
|
||||
{t(`statusBar.${item.action}`)}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/approval-screen.png",
|
||||
},
|
||||
{
|
||||
id: "delegation",
|
||||
title: t("landingPage.delegation"),
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="dark:text-gray-300">{t("landingPage.delegateInfo")}</p>
|
||||
<ol className="list-decimal pl-5 space-y-2">
|
||||
<li className="dark:text-gray-300">
|
||||
{t("landingPage.delegateNav")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{t("delegation.title")}
|
||||
</span>{" "}
|
||||
{t("landingPage.tab")}
|
||||
</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.selectColleague")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.setDates")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.setPerms")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.saveDelegate")}</li>
|
||||
</ol>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 mt-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<span className="font-semibold">{t("landingPage.note")}</span>{" "}
|
||||
{t("landingPage.autoExpire")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/delegation-screen.png",
|
||||
},
|
||||
{
|
||||
id: "collaboration",
|
||||
title: t("landingPage.collab"),
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="dark:text-gray-300">{t("landingPage.collabInfo")}</p>
|
||||
<ul className="list-disc pl-5 space-y-2">
|
||||
<li className="dark:text-gray-300">{t("landingPage.viewDocs")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.addComments")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.approve")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.trackChanges")}</li>
|
||||
</ul>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">
|
||||
{t("landingPage.realTime")}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||
{t("landingPage.liveChanges")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">
|
||||
{t("landingPage.notifications")}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||
{t("landingPage.alerts")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/collaboration-screen.png",
|
||||
},
|
||||
{
|
||||
id: "teeter",
|
||||
title: t("landingPage.signatureMgmt"),
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="dark:text-gray-300">{t("landingPage.leadersOnly")}</p>
|
||||
<ol className="list-decimal pl-5 space-y-2">
|
||||
<li className="dark:text-gray-300">
|
||||
{t("landingPage.goToTab")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{t("landingPage.signatureTitle")}
|
||||
</span>{" "}
|
||||
{t("landingPage.tab")}
|
||||
</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.uploadTeeter")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.uploadSignature")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.defaultSignature")}</li>
|
||||
<li className="dark:text-gray-300">{t("landingPage.updateDesignation")}</li>
|
||||
</ol>
|
||||
<div className="bg-red-50 dark:bg-red-900/30 p-4 rounded-lg border border-red-200 dark:border-red-800 mt-4">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
<span className="font-semibold">{t("landingPage.security")}</span>{" "}
|
||||
{t("landingPage.encrypted")}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
image: "/images/teeter-screen.png",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
{/* Header */}
|
||||
<header className="bg-white dark:bg-gray-800 shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{t("landingPage.recordModule")}
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-gray-600 dark:text-gray-300">
|
||||
{t("landingPage.guide")}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
className="mt-4 md:mt-0 px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
{t("landingPage.back")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Navigation Tabs */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav className="-mb-px flex space-x-8 overflow-x-auto scrollbar-hidden">
|
||||
{steps.map((step) => (
|
||||
<button
|
||||
key={step.id}
|
||||
onClick={() => handleTabChange(step.id)}
|
||||
className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm cursor-pointer ${
|
||||
activeTab === step.id
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="mt-8 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Steps List */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="p-4 bg-primary">
|
||||
<h2 className="text-lg font-medium text-white">
|
||||
{t("landingPage.moduleGuide")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
onClick={() => handleTabChange(step.id)}
|
||||
className={`p-4 cursor-pointer transition-colors ${
|
||||
activeTab === step.id
|
||||
? "bg-primary-500/5"
|
||||
: "hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`h-8 w-8 rounded-full flex items-center justify-center mr-3 ${
|
||||
activeTab === step.id
|
||||
? "bg-primary text-white"
|
||||
: "bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{steps.findIndex((s) => s.id === step.id) + 1}
|
||||
</div>
|
||||
<h3 className="text-sm font-medium dark:text-gray-200">{step.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Tab Content */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{steps.find((step) => step.id === activeTab)?.title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 prose prose-sm max-w-none">
|
||||
{steps.find((step) => step.id === activeTab)?.content}
|
||||
</div>
|
||||
|
||||
{/* Image placeholder - replace with actual image */}
|
||||
<div className="mt-8 bg-gray-100 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex items-center justify-center">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{t("landingPage.screenshotOf")}{" "}
|
||||
{steps.find((step) => step.id === activeTab)?.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700 flex justify-between">
|
||||
{steps.findIndex((step) => step.id === activeTab) > 0 && (
|
||||
<button
|
||||
onClick={() =>
|
||||
handleTabChange(
|
||||
steps[
|
||||
steps.findIndex((step) => step.id === activeTab) - 1
|
||||
].id
|
||||
)
|
||||
}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
{t("landingPage.previous")}
|
||||
</button>
|
||||
)}
|
||||
{steps.findIndex((step) => step.id === activeTab) <
|
||||
steps.length - 1 && (
|
||||
<button
|
||||
onClick={() =>
|
||||
handleTabChange(
|
||||
steps[
|
||||
steps.findIndex((step) => step.id === activeTab) + 1
|
||||
].id
|
||||
)
|
||||
}
|
||||
className="ml-auto inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
{t("landingPage.next")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordManagementGuide;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { useState } from "react";
|
||||
import { FaExclamationCircle, FaArrowLeft } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Header from "./Header";
|
||||
import Footer from "./Footer";
|
||||
import { getComplaintById } from "../../shared/services/complaintService";
|
||||
|
||||
const KnowAs = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>comming soon</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowAs;
|
||||
@@ -0,0 +1,591 @@
|
||||
import { useMemo, useEffect, type ReactNode } from "react";
|
||||
|
||||
export interface FooterData {
|
||||
address: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Socials {
|
||||
facebook?: string;
|
||||
twitter?: string;
|
||||
linkedin?: string;
|
||||
instagram?: string;
|
||||
youtube?: string;
|
||||
telegram?: string;
|
||||
tiktok?: string;
|
||||
website?: string;
|
||||
}
|
||||
export type fileInfo = {
|
||||
size?: number;
|
||||
bucket?: string;
|
||||
fileName?: string;
|
||||
contentType?: string;
|
||||
originalname?: string;
|
||||
};
|
||||
export interface ModuleConfig {
|
||||
recordManagement: boolean;
|
||||
siteManagement: boolean;
|
||||
dms: boolean;
|
||||
performance: boolean;
|
||||
objective: boolean;
|
||||
complaint: boolean;
|
||||
}
|
||||
|
||||
export interface TenantConfig {
|
||||
appName: string;
|
||||
organizationName: string;
|
||||
canUseAttachmentFromDMS?: boolean;
|
||||
logo: string;
|
||||
primaryColor: string;
|
||||
secondaryColor?: string;
|
||||
footerText: string;
|
||||
footerData: FooterData;
|
||||
socials: Socials;
|
||||
welcomeMessage: string;
|
||||
moduleConfig?: Partial<ModuleConfig>;
|
||||
dashboardPreviewImage?: string;
|
||||
}
|
||||
export interface brandingDTO {
|
||||
organizationName: {
|
||||
am: string;
|
||||
en: string;
|
||||
};
|
||||
logo: {
|
||||
presigned: string;
|
||||
fileInfo: fileInfo;
|
||||
};
|
||||
primaryColor: string;
|
||||
secondaryColor?: string;
|
||||
footerText: string;
|
||||
footerData: FooterData;
|
||||
socials: Socials;
|
||||
welcomeMessage: string;
|
||||
loginImage?: {
|
||||
presigned: string;
|
||||
fileInfo: fileInfo;
|
||||
};
|
||||
favicon?: {
|
||||
presigned: string;
|
||||
fileInfo: fileInfo;
|
||||
};
|
||||
moduleConfig?: Partial<ModuleConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve effective module visibility for a tenant.
|
||||
* Record Management and Site Management default to enabled.
|
||||
* Complaint controls public landing-page complaint entry points only (not module navigation).
|
||||
* Other optional modules default to disabled unless enabled by the tenant.
|
||||
*/
|
||||
export const resolveModuleConfig = (config: TenantConfig): ModuleConfig => ({
|
||||
recordManagement: config.moduleConfig?.recordManagement ?? true,
|
||||
siteManagement: true,
|
||||
dms: config.moduleConfig?.dms ?? false,
|
||||
performance: config.moduleConfig?.performance ?? false,
|
||||
objective: config.moduleConfig?.objective ?? false,
|
||||
complaint: config.moduleConfig?.complaint ?? false,
|
||||
});
|
||||
|
||||
const defaultConfig: TenantConfig = {
|
||||
appName: "Smart Office",
|
||||
organizationName: "Smart Office",
|
||||
canUseAttachmentFromDMS: false,
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
secondaryColor: "#0f5a3a",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
|
||||
};
|
||||
|
||||
const tenantConfigs: Record<string, TenantConfig> = {
|
||||
localhost: {
|
||||
appName: "Smart Office",
|
||||
organizationName: "Addis Ababa City Administration",
|
||||
logo: "",
|
||||
primaryColor: "#115005",
|
||||
moduleConfig: {
|
||||
dms: true,
|
||||
performance: true,
|
||||
objective: true,
|
||||
complaint: true,
|
||||
},
|
||||
canUseAttachmentFromDMS: true,
|
||||
secondaryColor: "#dad9db",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"Welcome to the Smart Office of the Addis Ababa City Administration. Addis Ababa is Ethiopia’s capital and a center of public service, innovation, culture, and opportunity. Through this platform, we aim to strengthen efficient service delivery, modernize administrative workflows, improve transparency, and support responsive governance for residents, institutions, and stakeholders across the city.",
|
||||
},
|
||||
"127.0.0.1": {
|
||||
appName: "Smart Office EDR",
|
||||
organizationName: "Ethio-Djibouti Railways",
|
||||
canUseAttachmentFromDMS: true,
|
||||
logo: "/assets/edrlogo.png",
|
||||
primaryColor: "#DC143C",
|
||||
moduleConfig: {
|
||||
dms: true,
|
||||
performance: true,
|
||||
objective: true,
|
||||
complaint: true,
|
||||
},
|
||||
secondaryColor: "#0f5a3a",
|
||||
dashboardPreviewImage: "/edrheadoffice.jpg",
|
||||
footerText:
|
||||
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti.",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "9546",
|
||||
email: "edr_@edrsc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://edrsc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti. A Shareholders Agreement was signed on January 11, 2017, among public bodies and state enterprises from both nations, governed by Ethiopian commercial law.\n\nThe shareholders formed a Share Company with an initial capital of USD 500 million, dedicated to operating and maintaining the Addis Ababa–Djibouti Railway and providing freight and passenger transport services.",
|
||||
},
|
||||
"smartoffice.edrsc.com": {
|
||||
appName: "Smart Office EDR",
|
||||
organizationName: "Ethio-Djibouti Railways",
|
||||
canUseAttachmentFromDMS: true,
|
||||
logo: "/assets/edrlogo.png",
|
||||
primaryColor: "#13724D",
|
||||
moduleConfig: {
|
||||
dms: true,
|
||||
performance: false,
|
||||
objective: false,
|
||||
complaint: true,
|
||||
},
|
||||
secondaryColor: "#0f5a3a",
|
||||
dashboardPreviewImage: "/edrheadoffice.jpg",
|
||||
footerText:
|
||||
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti.",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "9546",
|
||||
email: "edr_@edrsc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "https://web.facebook.com/ethiodjiboutirailwaysc",
|
||||
twitter: "https://edrsc.com/",
|
||||
linkedin: "https://edrsc.com/",
|
||||
instagram: "https://edrsc.com/",
|
||||
youtube: "https://edrsc.com/",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://edrsc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti. A Shareholders Agreement was signed on January 11, 2017, among public bodies and state enterprises from both nations, governed by Ethiopian commercial law.\n\nThe shareholders formed a Share Company with an initial capital of USD 500 million, dedicated to operating and maintaining the Addis Ababa–Djibouti Railway and providing freight and passenger transport services.",
|
||||
},
|
||||
"smartoffice.eiar.gov.et": {
|
||||
appName: "Smart Office EIAR",
|
||||
organizationName: "Ethiopian Institute of Agricultural Research",
|
||||
logo: "/assets/eiarLogo.jpg",
|
||||
primaryColor: "#388e4b",
|
||||
moduleConfig: { dms: false, performance: false, objective: false },
|
||||
secondaryColor: "#fff",
|
||||
footerText:
|
||||
"The Ethiopian Institute of Agricultural Research (EIAR) is one of the oldest and largest agricultural research institutes in Africa. EIAR has evolved through several stages since its first initiation during the late 1940s, following the establishment of agricultural and technical schools at Ambo and Jimma. In 1955, a full-fledged agricultural experiment station was established at Debre Zeit (now named Debre Zeit Agricultural Research Center) under the then Imperial College of Agricultural and mechanical Arts (now called Haramaya University) and had been continued as the major research entity until the mid-1960s. In 1966, Institute of Agricultural Research (IAR) was established as the first nationally coordinated agricultural research institute in Ethiopia. IAR was established with a mission to formulate national agricultural research guidelines, coordinate national agricultural research system, and undertake research in its centers and sub-centers located in various agro-ecological zones of Ethiopia.",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251-116-454441",
|
||||
email: "eiar.gov.et",
|
||||
},
|
||||
canUseAttachmentFromDMS: false,
|
||||
socials: {
|
||||
facebook: "https://web.facebook.com/EIARPR",
|
||||
website: "http://www.eiar.gov.et",
|
||||
twitter: "http://www.eiar.gov.et",
|
||||
linkedin: "http://www.eiar.gov.et",
|
||||
instagram: "http://www.eiar.gov.et",
|
||||
youtube: "http://www.eiar.gov.et",
|
||||
},
|
||||
|
||||
welcomeMessage:
|
||||
"welcome you to the SmartOffice of the Ethiopian Institute of Agricultural Research. The Institute, since its establishment in 1966, has released over more 3000 agricultural technologies and improved farming practices by undertaking scientific research activities on various areas in crop, livestock, land and water, biotechnology, climate, farm machinery and agricultural economics. Of these technologies, 1190 are crop varieties and the rest include livestock breeds, pest and disease control methods, crop production and livestock husbandry methods, pre- and post-harvest technologies and recommendations. EIAR, in the five decades of its existence, has reached a large number of beneficiaries with its technologies and information found in different agro-ecologies throughout the country. The Institute has engaged itself in multiplication of initial technologies based on the demand created by the beneficiaries and supply to public and private technology multiplication actors for their wider reproduction and distribution.",
|
||||
},
|
||||
"smartoffice.ebi.gov.et": {
|
||||
appName: "EBI Smart Office",
|
||||
organizationName: "Ethiopian Biodiversity Institute - EBI",
|
||||
logo: "/assets/ebiLogoWhite.png",
|
||||
primaryColor: "#2a741d",
|
||||
moduleConfig: { dms: false, performance: false, objective: false },
|
||||
secondaryColor: "#2c246d",
|
||||
footerText: "Welcome to the Center of Origin & Diversity, Ethiopia",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251-116-615607",
|
||||
email: " info@ebi.gov.et",
|
||||
},
|
||||
canUseAttachmentFromDMS: false,
|
||||
socials: {
|
||||
facebook: "https://www.facebook.com/EthiopiaEBI",
|
||||
twitter: "https://x.com/EthiopiaEBI",
|
||||
linkedin: "https://www.linkedin.com/company/EthiopiaEBI",
|
||||
youtube: "https://www.youtube.com/@EthiopiaEBI",
|
||||
instagram: "https://www.youtube.com/@EthiopiaEBI",
|
||||
website: "https://ebi.gov.et",
|
||||
telegram: "https://t.me/EthiopiaEBI",
|
||||
tiktok: "https://www.tiktok.com/@ethiopiaebi",
|
||||
},
|
||||
welcomeMessage:
|
||||
"Ethiopia is recognized as one of the world’s most biodiverse nations. Our rugged highlands, vast lowlands, and the rift valley lakes hold the genetic codes of the wild relatives of our staple food, and the endemic wildlife that defines our national identity. The Ethiopian Biodiversity Institute - EBI bears the profound responsibility of being the steward of these natural assets since its establishment in the 1970s.",
|
||||
},
|
||||
"smartoffice.aaca.gov.et": {
|
||||
appName: "Smart Office",
|
||||
organizationName: "Addis Ababa City Administration",
|
||||
logo: "",
|
||||
primaryColor: "#115005",
|
||||
moduleConfig: { dms: false, performance: false, objective: false },
|
||||
secondaryColor: "#dad9db",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
canUseAttachmentFromDMS: false,
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"Welcome to the Smart Office of the Addis Ababa City Administration. Addis Ababa is Ethiopia’s capital and a center of public service, innovation, culture, and opportunity. Through this platform, we aim to strengthen efficient service delivery, modernize administrative workflows, improve transparency, and support responsive governance for residents, institutions, and stakeholders across the city.",
|
||||
},
|
||||
"smartoffice.efd.moa.gov.et": {
|
||||
appName: "Smart Office EFD",
|
||||
organizationName: "Ethiopian Forest Development",
|
||||
logo: "/assets/efdlogo.jpg",
|
||||
primaryColor: "#006400",
|
||||
moduleConfig: { dms: false, performance: false, objective: false },
|
||||
secondaryColor: "#a52a2a",
|
||||
footerText:
|
||||
"Ethiopian Forestry Development (EFD) is an autonomous federal institution, established by Proclamation No. 1263/2021 on 25th January, as referred on article 81-No. 8 and by the federal government of Ethiopia council of ministers regulation No. 505/2022. EFD was resulted by merging the former research institute (The Ethiopian Environment and Forest Research Institute (EEFRI) together with the forestry sector from the then (Environment, Forest and climate change commission) having the following powers and duties.",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "9546",
|
||||
email: "dg-office@efd.gov.et",
|
||||
},
|
||||
canUseAttachmentFromDMS: false,
|
||||
socials: {
|
||||
facebook:
|
||||
"https://web.facebook.com/Ethiopian-Forestry-Development-EFD-100064767336706/?_rdc=1&_rdr#",
|
||||
twitter: "Ethiopian Forestry Development@EthiopianFores1",
|
||||
youtube: "https://www.youtube.com/@infoefd",
|
||||
website: "https://www.efd.gov.et",
|
||||
},
|
||||
welcomeMessage:
|
||||
"Organized forestry research in Ethiopia was started by the establishment of Forestry Research Centre (FRC) and the then Wood Utilization Research Centre (WUARC) in 1975 and 1979, respectively under the Forestry and Wildlife Conservation Development Authority (FaWCDA). The centres were incorporated to the then Ministry of Natural Resources Development and Environmental Protection in 1992 and again re-transferred to the Ministry of Agriculture in 1995. The Federal Government of Ethiopia reorganized the National Agricultural Research System and established the then Ethiopian Agricultural Research Organization (EARO) in 1997 (Negarit Gazeta, 1997). As a result, FRC and WUARC were transferred to EARO as one research centre (FRC) and one of the research sectors of EARO (now Ethiopian Institute of Agricultural Research (EIAR)). Then, the Government of Ethiopia found it necessary to give due attention to activities of environmental protection and forest development, protection and utilization by linking forestry research with environmental protection research at an institutional level for the attainment of the objectives of the Government that resulted in the establishment of the Ministry of Environment, Forest and Climate Change.",
|
||||
},
|
||||
"smartoffice.moa.gov.et": {
|
||||
appName: "Smart Office MOA",
|
||||
organizationName: "Ministry of Agriculture",
|
||||
logo: "/assets/ministryOfAgriculture.jpg",
|
||||
primaryColor: "#006400",
|
||||
moduleConfig: {
|
||||
dms: false,
|
||||
performance: false,
|
||||
objective: true,
|
||||
recordManagement: false,
|
||||
},
|
||||
secondaryColor: "#a52a2a",
|
||||
footerText:
|
||||
"The Ministry of Agriculture (MoA) is a federal government institution of Ethiopia responsible for leading and coordinating the country's agricultural development. The Ministry is mandated to formulate policies, strategies, and programs that enhance agricultural productivity, ensure food security, promote sustainable natural resource management, and support rural transformation. Through its various sectors and agencies, the Ministry oversees crop and livestock development, agricultural extension services, research coordination, and the implementation of national agricultural initiatives aimed at improving the livelihoods of farmers and contributing to the country's economic growth.",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "9546",
|
||||
email: "dg-office@efd.gov.et",
|
||||
},
|
||||
canUseAttachmentFromDMS: false,
|
||||
socials: {
|
||||
facebook: "https://web.facebook.com/MoAEthiopia/?_rdc=1&_rdr#",
|
||||
twitter: "https://x.com/MoA_Ethiopia",
|
||||
youtube: "https://www.youtube.com/@publicrelationmoa",
|
||||
website: "https://www.moa.gov.et",
|
||||
},
|
||||
welcomeMessage:
|
||||
"The Ministry of Agriculture (MoA) has played a central role in Ethiopia’s agricultural development and transformation efforts. Over the years, the Ministry has undergone various reforms and restructuring initiatives to strengthen the country's agricultural sector and improve food security. The Ministry is responsible for guiding agricultural research, extension services, livestock and crop development, natural resource management, and rural development programs. It works closely with research institutions, regional bureaus, development partners, and stakeholders to promote sustainable agricultural practices, increase productivity, and support the livelihoods of farmers and pastoral communities. Through its ongoing efforts, the Ministry continues to contribute significantly to Ethiopia’s economic growth, environmental sustainability, and national development objectives.",
|
||||
},
|
||||
"smartofficedev.moa.gov.et": {
|
||||
appName: "Smart Office MOA",
|
||||
organizationName: "Ministry of Agriculture",
|
||||
logo: "/assets/ministryOfAgriculture.jpg",
|
||||
primaryColor: "#006400",
|
||||
moduleConfig: {
|
||||
dms: false,
|
||||
performance: false,
|
||||
objective: true,
|
||||
recordManagement: true,
|
||||
},
|
||||
secondaryColor: "#a52a2a",
|
||||
footerText:
|
||||
"The Ministry of Agriculture (MoA) is a federal government institution of Ethiopia responsible for leading and coordinating the country's agricultural development. The Ministry is mandated to formulate policies, strategies, and programs that enhance agricultural productivity, ensure food security, promote sustainable natural resource management, and support rural transformation. Through its various sectors and agencies, the Ministry oversees crop and livestock development, agricultural extension services, research coordination, and the implementation of national agricultural initiatives aimed at improving the livelihoods of farmers and contributing to the country's economic growth.",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "9546",
|
||||
email: "dg-office@efd.gov.et",
|
||||
},
|
||||
canUseAttachmentFromDMS: false,
|
||||
socials: {
|
||||
facebook: "https://web.facebook.com/MoAEthiopia/?_rdc=1&_rdr#",
|
||||
twitter: "https://x.com/MoA_Ethiopia",
|
||||
youtube: "https://www.youtube.com/@publicrelationmoa",
|
||||
website: "https://www.moa.gov.et",
|
||||
},
|
||||
welcomeMessage:
|
||||
"The Ministry of Agriculture (MoA) has played a central role in Ethiopia’s agricultural development and transformation efforts. Over the years, the Ministry has undergone various reforms and restructuring initiatives to strengthen the country's agricultural sector and improve food security. The Ministry is responsible for guiding agricultural research, extension services, livestock and crop development, natural resource management, and rural development programs. It works closely with research institutions, regional bureaus, development partners, and stakeholders to promote sustainable agricultural practices, increase productivity, and support the livelihoods of farmers and pastoral communities. Through its ongoing efforts, the Ministry continues to contribute significantly to Ethiopia’s economic growth, environmental sustainability, and national development objectives.",
|
||||
},
|
||||
"triadms-dev.smartoffice.aaca.gov.et": {
|
||||
appName: "Smart Office Tria",
|
||||
organizationName: "Tria Trading PLC",
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
moduleConfig: { dms: true, performance: true, objective: true },
|
||||
secondaryColor: "#0f5a3a",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
canUseAttachmentFromDMS: true,
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
|
||||
},
|
||||
"triadms.smartoffice.aaca.gov.et": {
|
||||
appName: "Smart Office Tria",
|
||||
organizationName: "Tria Trading PLC",
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
moduleConfig: { dms: true, performance: true, objective: true },
|
||||
secondaryColor: "#0f5a3a",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
|
||||
},
|
||||
"smart-dev.smart.aaca.gov.et": {
|
||||
appName: "Smart Office Tria",
|
||||
organizationName: "Tria Trading PLC",
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
moduleConfig: { dms: true, performance: true, objective: true },
|
||||
secondaryColor: "#0f5a3a",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
|
||||
},
|
||||
"performance-dev.smart.aaca.gov.et": {
|
||||
appName: "Smart Office Tria",
|
||||
organizationName: "Tria Trading PLC",
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
moduleConfig: { dms: true, performance: true, objective: true },
|
||||
secondaryColor: "#0f5a3a",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
|
||||
},
|
||||
"performance.smart.aaca.gov.et": {
|
||||
appName: "Smart Office Tria",
|
||||
organizationName: "Tria Trading PLC",
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
moduleConfig: { dms: true, performance: true, objective: true },
|
||||
secondaryColor: "#0f5a3a",
|
||||
footerText: "COMMITED TO EXCELLENCE",
|
||||
footerData: {
|
||||
address: "Addis Ababa, Ethiopia",
|
||||
phone: "+251955232323",
|
||||
email: "info@triaplc.com",
|
||||
},
|
||||
socials: {
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
telegram: "",
|
||||
tiktok: "",
|
||||
website: "https://triaplc.com/",
|
||||
},
|
||||
welcomeMessage:
|
||||
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
|
||||
},
|
||||
};
|
||||
|
||||
/** Resolve tenant config from hostname (defaults to localhost config). */
|
||||
export const resolveTenantConfig = (
|
||||
hostname: string = typeof window !== "undefined"
|
||||
? window.location.hostname
|
||||
: "localhost",
|
||||
): TenantConfig => {
|
||||
// Normalize host input so callers can pass full URLs or mixed-case values safely.
|
||||
const normalizedHost = hostname
|
||||
?.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/.*$/, "")
|
||||
.replace(/:\d+$/, "");
|
||||
|
||||
return tenantConfigs[normalizedHost] || defaultConfig;
|
||||
};
|
||||
|
||||
/** Apply tenant theme tokens to the document root (CSS variables, title, favicon). */
|
||||
export const applyTenantTheme = (config: TenantConfig) => {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
const primary =
|
||||
config.primaryColor?.trim() || "var(--primary-default, #18aa9d)";
|
||||
|
||||
root.style.setProperty("--primary", primary);
|
||||
root.style.setProperty("--ring", primary);
|
||||
root.style.setProperty("--sidebar-primary", primary);
|
||||
root.style.setProperty("--accent", primary);
|
||||
root.style.setProperty("--chart-1", primary);
|
||||
|
||||
if (config.organizationName) {
|
||||
document.title = config.organizationName;
|
||||
}
|
||||
|
||||
if (config.logo) {
|
||||
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
||||
|
||||
if (!link) {
|
||||
link = document.createElement("link");
|
||||
link.rel = "icon";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
link.href = config.logo;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply tenant theme before React mounts so deep-linked routes (e.g. record-management)
|
||||
// keep branding on hard refresh without relying on login/landing page components.
|
||||
if (typeof window !== "undefined") {
|
||||
applyTenantTheme(resolveTenantConfig());
|
||||
}
|
||||
|
||||
export const useTenantConfig = () => {
|
||||
const hostname = useMemo(() => window.location.hostname, []);
|
||||
|
||||
const config = useMemo(() => resolveTenantConfig(hostname), [hostname]);
|
||||
|
||||
useEffect(() => {
|
||||
applyTenantTheme(config);
|
||||
}, [config]);
|
||||
|
||||
return {
|
||||
config,
|
||||
hostname,
|
||||
};
|
||||
};
|
||||
|
||||
/** Ensures tenant theme is applied on every route, including lazy-loaded modules. */
|
||||
export const TenantConfigProvider = ({ children }: { children: ReactNode }) => {
|
||||
useTenantConfig();
|
||||
return children;
|
||||
};
|
||||
@@ -0,0 +1,396 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const WorkflowSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const steps = [
|
||||
{
|
||||
id: "01",
|
||||
name: t("landingPage.recordCreation"),
|
||||
description: t("landingPage.recordCreationDesc"),
|
||||
status: "complete",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "02",
|
||||
name: t("landingPage.teamLeaderReview"),
|
||||
description: t("landingPage.teamLeaderReviewDesc"),
|
||||
status: "complete",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "03",
|
||||
name: t("landingPage.directorApproval"),
|
||||
description: t("landingPage.directorApprovalDesc"),
|
||||
status: "current",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "04",
|
||||
name: t("landingPage.deputyHeadReview"),
|
||||
description: t("landingPage.deputyHeadReviewDesc"),
|
||||
status: "upcoming",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "05",
|
||||
name: t("landingPage.recordOfficerProcessing"),
|
||||
description: t("landingPage.recordOfficerProcessingDesc"),
|
||||
status: "upcoming",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="py-16 bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="lg:text-center mb-16">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary/10 text-primary animate-pulse">
|
||||
{t("landingPage.streamlinedProcess")}
|
||||
</span>
|
||||
<h2 className="mt-4 text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
|
||||
<span className="block">
|
||||
{t("landingPage.recordManagementApprovalWorkflows")}
|
||||
</span>
|
||||
<span className="block text-primary">
|
||||
{t("landingPage.workflow")}
|
||||
</span>
|
||||
</h2>
|
||||
<p className="mt-6 max-w-3xl text-xl text-gray-600 dark:text-gray-300 lg:mx-auto">
|
||||
{t("landingPage.smartOfficeDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<div className="relative">
|
||||
{/* Progress bar */}
|
||||
<div className="hidden md:block absolute top-0 left-16 h-full w-0.5 bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full bg-primary transition-all duration-1000 ease-in-out"
|
||||
style={{ height: "60%" }} // Adjust based on current step
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-10 md:space-y-12">
|
||||
{steps.map((step, stepIdx) => (
|
||||
<li
|
||||
key={step.name}
|
||||
className="relative group transition-all duration-300 hover:scale-[1.02]"
|
||||
data-aos="fade-up"
|
||||
data-aos-delay={stepIdx * 100}
|
||||
>
|
||||
<div className="relative flex items-start md:items-center">
|
||||
{/* Step indicator */}
|
||||
<div className="flex-shrink-0 relative z-10">
|
||||
<div
|
||||
className={`flex items-center justify-center w-12 h-12 rounded-full transition-all duration-300 shadow-lg ${
|
||||
step.status === "complete"
|
||||
? "bg-primary text-white transform group-hover:scale-110"
|
||||
: step.status === "current"
|
||||
? "bg-white dark:bg-gray-800 border-4 border-primary shadow-[var(--primary)]/30"
|
||||
: "bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 group-hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
{step.status === "complete" ? (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
) : step.status === "current" ? (
|
||||
<div className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-primary opacity-75"></div>
|
||||
) : step.status === "upcoming" ? (
|
||||
<span className="text-gray-400 dark:text-gray-500 font-medium">{step.id}</span>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500">{step.icon}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div
|
||||
className={`ml-6 p-6 rounded-xl flex-1 transition-all duration-300 ${
|
||||
step.status === "current"
|
||||
? "bg-white dark:bg-gray-800 border-l-4 border-primary shadow-lg"
|
||||
: "bg-white dark:bg-gray-800 shadow-md group-hover:shadow-lg"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span
|
||||
className={`text-xs font-semibold tracking-wider ${
|
||||
step.status === "complete"
|
||||
? "text-primary"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{t("landingPage.step")} {step.id}
|
||||
</span>
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white mt-1">
|
||||
{step.name}
|
||||
</h3>
|
||||
</div>
|
||||
{step.status === "current" && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white animate-pulse">
|
||||
{t("statusBar.In Progress")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-300">{step.description}</p>
|
||||
|
||||
{step.status === "current" && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||
<div className="flex space-x-4">
|
||||
<button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-700 transition-colors">
|
||||
{t("statusBar.Approve")}
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
|
||||
{t("landingPage.requestChanges")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features section */}
|
||||
<div className="mt-20 bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
<div className="p-10 bg-gradient-to-br from-primary to-primary-800 text-white">
|
||||
<h3 className="text-2xl font-bold mb-6">
|
||||
{t("landingPage.workflowAutomationBenefits")}
|
||||
</h3>
|
||||
<p className="mb-8 opacity-90">
|
||||
{t("landingPage.workflowAutomationDesc")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-white/20">
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("landingPage.fasterApprovals")}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm opacity-80">
|
||||
{t("landingPage.reduceDelays")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-white/20">
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("landingPage.auditTrail")}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm opacity-80">
|
||||
{t("landingPage.completeRecord")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-10">
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{t("landingPage.advancedFeatures")}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-xl bg-primary-500/5 text-primary">
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{t("landingPage.slaMonitoring")}
|
||||
</h4>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-300">
|
||||
{t("landingPage.slaMonitoringDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-xl bg-primary-500/5 text-primary">
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{t("landingPage.conditionalRouting")}
|
||||
</h4>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-300">
|
||||
{t("landingPage.conditionalRoutingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-xl bg-primary-500/5 text-primary">
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{t("delegation.title")}
|
||||
</h4>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-300">
|
||||
{t("landingPage.temporaryApprovalDelegation")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowSection;
|
||||
Reference in New Issue
Block a user