mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
user management ui
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CreateMessagePayload, MessageFormData } from "../types/messageTypes";
|
||||
import { toast } from "sonner";
|
||||
import useSettings from "@/record-management/components/hooks/useSettings";
|
||||
import { EmployeeWithUnitDto } from "@/user-management/dto/employees/employees";
|
||||
import { KeyValue } from "@/record-management/types/recordSelectTypes";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMessage } from "../hooks/useMessage";
|
||||
import MessageForm from "./Form";
|
||||
|
||||
export const AddMessage = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
} = useForm<MessageFormData>({
|
||||
defaultValues: {
|
||||
messageTitleEn: "",
|
||||
messageTitleAm: "",
|
||||
messageContentAm: "",
|
||||
messageContentEn: "",
|
||||
employeeId: "",
|
||||
staffType: "",
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
const { currentEmployeesResponse } = useSettings();
|
||||
const localizedName = useLocalizedName();
|
||||
const navigate = useNavigate();
|
||||
const { createItem } = useMessage();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const employeesList: KeyValue[] =
|
||||
currentEmployeesResponse?.items.map((employee: EmployeeWithUnitDto) => ({
|
||||
label: localizedName(employee.user.name),
|
||||
value: employee?.id ?? "",
|
||||
employeePositionId: employee.employeePositions?.[0]?.id ?? "",
|
||||
})) || [];
|
||||
const handleCreateSubmit = async (MessageFormData: MessageFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const imageFile = MessageFormData.image?.[0];
|
||||
if (!imageFile) {
|
||||
throw new Error("images are required");
|
||||
}
|
||||
const payload: CreateMessagePayload = {
|
||||
messageTitle: {
|
||||
am: MessageFormData.messageTitleAm || "",
|
||||
en: MessageFormData.messageTitleEn || "",
|
||||
},
|
||||
messageContent: {
|
||||
am: MessageFormData.messageContentAm || "",
|
||||
en: MessageFormData.messageContentEn || "",
|
||||
},
|
||||
fileInfo: {
|
||||
fileName: imageFile.name,
|
||||
contentType: imageFile.type,
|
||||
size: imageFile.size,
|
||||
originalname: imageFile.name,
|
||||
},
|
||||
isActive: MessageFormData.isActive,
|
||||
staffType: MessageFormData.staffType,
|
||||
employeeId: MessageFormData.employeeId,
|
||||
};
|
||||
await createItem({
|
||||
data: payload,
|
||||
imageFile: imageFile,
|
||||
});
|
||||
toast.success(" created successfully");
|
||||
navigate(-1);
|
||||
} catch (error) {
|
||||
toast.error("Failed to create Message");
|
||||
console.error("Creation error:", error);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
const onCancel = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
return (
|
||||
<MessageForm
|
||||
handleSubmit={handleSubmit(handleCreateSubmit)}
|
||||
handleCancel={onCancel}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
control={control}
|
||||
register={register}
|
||||
errors={errors}
|
||||
isSubmitting={isSubmitting}
|
||||
employeesList={employeesList}
|
||||
label="Add Message"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
Control,
|
||||
FieldErrors,
|
||||
useForm,
|
||||
UseFormRegister,
|
||||
UseFormReset,
|
||||
UseFormSetValue,
|
||||
UseFormWatch,
|
||||
} from "react-hook-form";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { EmployeeWithUnitDto } from "@/user-management/dto/employees/employees";
|
||||
import { KeyValue } from "@/record-management/types/recordSelectTypes";
|
||||
import useSettings from "@/record-management/components/hooks/useSettings";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import {
|
||||
CreateMessagePayload,
|
||||
MessageFormData,
|
||||
MessageItem,
|
||||
} from "../types/messageTypes";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMessage } from "../hooks/useMessage";
|
||||
import { toast } from "sonner";
|
||||
import MessageForm from "./Form";
|
||||
|
||||
export interface FormProps {
|
||||
watch: UseFormWatch<MessageFormData>;
|
||||
control: Control<MessageFormData>;
|
||||
setValue: UseFormSetValue<MessageFormData>;
|
||||
register: UseFormRegister<MessageFormData>;
|
||||
errors: FieldErrors<MessageFormData>;
|
||||
reset?: UseFormReset<MessageFormData>;
|
||||
handleSubmit: (e: React.FormEvent) => void;
|
||||
handleCancel: () => void;
|
||||
isSubmitting: boolean;
|
||||
employeesList: KeyValue[];
|
||||
item?: MessageItem;
|
||||
label: string;
|
||||
}
|
||||
export const EditMessage = () => {
|
||||
const { id } = useParams();
|
||||
const { singleItem: item, updateItem } = useMessage(undefined, id);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
messageTitleEn: item?.messageTitle?.en || "",
|
||||
messageTitleAm: item?.messageTitle?.am || "",
|
||||
messageContentAm: item?.messageContent?.am || "",
|
||||
messageContentEn: item?.messageContent?.en || "",
|
||||
employeeId: item?.employeeId || "",
|
||||
isActive: item?.isActive ?? true,
|
||||
staffType: item?.staffType || "",
|
||||
image: undefined,
|
||||
}),
|
||||
[item] // 👈 recompute only when item changes
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<MessageFormData>({
|
||||
defaultValues,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
reset(defaultValues);
|
||||
}
|
||||
}, [item, defaultValues, reset]);
|
||||
|
||||
const { currentEmployeesResponse } = useSettings();
|
||||
const localizedName = useLocalizedName();
|
||||
const employeesList: KeyValue[] =
|
||||
currentEmployeesResponse?.items.map((employee: EmployeeWithUnitDto) => ({
|
||||
label: localizedName(employee.user.name),
|
||||
value: employee.id || "",
|
||||
employeePositionId: employee.employeePositions?.[0]?.id ?? "",
|
||||
})) || [];
|
||||
|
||||
const handleEditSubmit = async (MessageFormData: MessageFormData) => {
|
||||
if (!item) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const imageFile = MessageFormData.image?.[0];
|
||||
const payload: CreateMessagePayload = {
|
||||
messageTitle: {
|
||||
am: MessageFormData.messageTitleAm || "",
|
||||
en: MessageFormData.messageTitleEn || "",
|
||||
},
|
||||
messageContent: {
|
||||
am: MessageFormData.messageContentAm || "",
|
||||
en: MessageFormData.messageContentEn || "",
|
||||
},
|
||||
fileInfo: imageFile
|
||||
? {
|
||||
fileName: imageFile.name,
|
||||
contentType: imageFile.type,
|
||||
size: imageFile.size,
|
||||
originalname: imageFile.name,
|
||||
}
|
||||
: undefined,
|
||||
isActive: MessageFormData.isActive,
|
||||
staffType: MessageFormData.staffType,
|
||||
employeeId: MessageFormData.employeeId,
|
||||
};
|
||||
|
||||
await updateItem({
|
||||
id: item?.id,
|
||||
data: payload,
|
||||
...(imageFile && { imageFile }),
|
||||
});
|
||||
|
||||
toast.success("message updated successfully");
|
||||
navigate(-1);
|
||||
} catch (error) {
|
||||
toast.error("Failed to update about us");
|
||||
console.error("Update error:", error);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
const onCancel = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
return (
|
||||
<MessageForm
|
||||
handleSubmit={handleSubmit(handleEditSubmit)}
|
||||
handleCancel={onCancel}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
control={control}
|
||||
register={register}
|
||||
errors={errors}
|
||||
isSubmitting={isSubmitting}
|
||||
employeesList={employeesList}
|
||||
reset={reset}
|
||||
item={item}
|
||||
label="Edit Message"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import {
|
||||
FormInputBox,
|
||||
FormSelector,
|
||||
FormTextBox,
|
||||
FormToggle,
|
||||
ImageUpload,
|
||||
Section,
|
||||
} from "../components/FormInputBox";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProps } from "./EditMessage";
|
||||
import { ContactUsList } from "../types/contactUs";
|
||||
|
||||
const MessageForm = ({
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
register,
|
||||
errors,
|
||||
isSubmitting,
|
||||
employeesList,
|
||||
reset,
|
||||
item,
|
||||
label,
|
||||
}: FormProps) => {
|
||||
const MessageType: ContactUsList[] = [
|
||||
{
|
||||
label: "Office Head",
|
||||
value: "office_head",
|
||||
},
|
||||
{
|
||||
label: "Deputy",
|
||||
value: "deputy",
|
||||
},
|
||||
];
|
||||
const imageFile = watch("image")?.[0];
|
||||
const [imagePreview, setImage1Preview] = useState<string>(
|
||||
item?.presigned || ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.presigned) {
|
||||
setImage1Preview(item.presigned);
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
// 🔹 when user picks a new image, show it
|
||||
useEffect(() => {
|
||||
if (imageFile) {
|
||||
const url = URL.createObjectURL(imageFile);
|
||||
setImage1Preview(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
}
|
||||
}, [imageFile]);
|
||||
useEffect(() => {
|
||||
if (employeesList.length && !watch("employeeId")) {
|
||||
setValue("employeeId", employeesList[0].value);
|
||||
}
|
||||
}, [employeesList, setValue, watch]);
|
||||
return (
|
||||
<div className="max-h-[80vh] overflow-y-auto p-6 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* message type */}
|
||||
<FormSelector
|
||||
id="staffType"
|
||||
label="Message Type"
|
||||
error="Type is Required"
|
||||
errors={errors}
|
||||
register={register}
|
||||
control={control}
|
||||
item={MessageType}
|
||||
/>
|
||||
{/* Employee Selection */}
|
||||
<FormSelector
|
||||
id="employeeId"
|
||||
label="Select Employee"
|
||||
error="Employee is Required"
|
||||
errors={errors}
|
||||
register={register}
|
||||
control={control}
|
||||
item={employeesList}
|
||||
/>
|
||||
{/* Title Section */}
|
||||
<FormInputBox
|
||||
idEn="messageTitleEn"
|
||||
idAm="messageTitleAm"
|
||||
label="Title"
|
||||
error="Title is Required"
|
||||
errors={errors}
|
||||
register={register}
|
||||
/>
|
||||
|
||||
{/* Content Section */}
|
||||
<FormTextBox
|
||||
idAm="messageContentAm"
|
||||
idEn="messageContentEn"
|
||||
label="Content"
|
||||
error="Content is Required"
|
||||
errors={errors}
|
||||
register={register}
|
||||
/>
|
||||
{/* isActive Toggle */}
|
||||
<FormToggle
|
||||
idAm="isActive"
|
||||
label="Status"
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
/>
|
||||
|
||||
{/* Image Upload */}
|
||||
<Section title="Image">
|
||||
<ImageUpload
|
||||
label="Experience Image*"
|
||||
preview={imagePreview}
|
||||
existingName={item?.fileInfo?.originalname}
|
||||
register={register("image")}
|
||||
error={errors.image?.message}
|
||||
onView={() => window.open(imagePreview, "_blank")}
|
||||
id="image"
|
||||
isRequired={false}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
...
|
||||
</span>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MessageForm;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { Trash2, Edit, Eye } from "lucide-react";
|
||||
import { MessageItem } from "../types/messageTypes";
|
||||
import { t } from "i18next";
|
||||
import i18n from "@/i18n";
|
||||
export const MessageColumnDefn = ({
|
||||
navigate,
|
||||
onDelete,
|
||||
}: {
|
||||
navigate: (id: string) => void;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}): ColumnDef<MessageItem>[] => [
|
||||
{
|
||||
header: () => t("toogle.title"),
|
||||
cell: ({ row }) => {
|
||||
const rowData = row.original;
|
||||
const lang = i18n.language;
|
||||
return (
|
||||
<span>
|
||||
{lang === "en" ? rowData.messageTitle.en : rowData.messageTitle.am}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
accessorKey: "title",
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: () => t("toogle.status"),
|
||||
cell: ({ row }) => {
|
||||
const rowData = row.original;
|
||||
return (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
rowData.isActive
|
||||
? "bg-primary-100 text-primary-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{rowData.isActive ? "Active" : "Inactive"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: () => t("toogle.createdAt"),
|
||||
cell: ({ row }) => {
|
||||
const rowData = row.original;
|
||||
const lang = i18n.language;
|
||||
const amDate = rowData?.amharicCreatedAt;
|
||||
const enDate = new Date(rowData.createdAt).toLocaleString("en-US", {
|
||||
month: "short", // "Oct"
|
||||
day: "numeric", // "12"
|
||||
year: "numeric", // "2014"
|
||||
hour: "numeric", // "8"
|
||||
minute: "2-digit", // "12"
|
||||
hour12: true, // "PM" format
|
||||
});
|
||||
const date = lang === "am" ? amDate : enDate;
|
||||
return <span className="text-sm text-gray-500">{date}</span>;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "Actions",
|
||||
header: () => t("toogle.Actions"),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/user-management/web-management/LeadershipMessage/ViewLeadershipMessage/${row.original.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/user-management/web-management/LeadershipMessage/EditLeadershipMessage/${row.original.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(row.original.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import { CirclePlus } from "lucide-react";
|
||||
import { MessageColumnDefn } from "./MessageColumn";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AdvancedTable } from "@/shared/common/ui/table/AdvancedTable";
|
||||
import { useMemo, useState } from "react";
|
||||
import Loader from "@/record-management/components/Loader/loader";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { useMessage } from "../hooks/useMessage";
|
||||
export const Messages = () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const pageSize = 10;
|
||||
const { t } = useTranslation();
|
||||
const baseParams = useMemo(() => {
|
||||
const params: Record<string, any> = {
|
||||
skip: pageIndex * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: "createdAt:DESC",
|
||||
};
|
||||
|
||||
return params;
|
||||
}, [pageIndex]);
|
||||
|
||||
const { items, refetch, deleteItem, error, isLoading, count } =
|
||||
useMessage(baseParams);
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPageIndex(newPage);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between mb-4 border-b pb-3 border-gray-200 dark:border-gray-700">
|
||||
<h1 className="text-xl font-semibold text-gray-800 dark:text-gray-200">
|
||||
{t("officeHead.title")}
|
||||
</h1>
|
||||
{
|
||||
<NavLink to="/user-management/web-Management/LeadershipMessage/AddLeadershipMessage">
|
||||
<Button
|
||||
variant="addRecordBtn"
|
||||
className="flex items-center gap-1 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition-all"
|
||||
>
|
||||
<CirclePlus className="w-4 h-4" />
|
||||
<span>{t("officeHead.addMessage")}</span>
|
||||
</Button>
|
||||
</NavLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-48">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-center text-red-500 text-sm mt-4">
|
||||
{t("userRecord.Error loading records.")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && (
|
||||
<div className="mt-2">
|
||||
<AdvancedTable
|
||||
columns={MessageColumnDefn({
|
||||
navigate,
|
||||
onDelete: deleteItem,
|
||||
})}
|
||||
data={items || []}
|
||||
tableName="LeadershipMessages"
|
||||
toolBarPosition="right"
|
||||
extraToolbar={<div className="flex gap-2"></div>}
|
||||
itemCount={count || 2}
|
||||
pageIndex={pageIndex}
|
||||
onPageChange={handlePageChange}
|
||||
nextFunction={() => handlePageChange(pageIndex + 1)}
|
||||
prevFunction={() => handlePageChange(Math.max(pageIndex - 1, 0))}
|
||||
refresh={refetch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import { MessageItem } from "../types/messageTypes";
|
||||
|
||||
interface ViewMessageProps {
|
||||
item: MessageItem;
|
||||
}
|
||||
|
||||
export const ViewMessage = ({ item }: ViewMessageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const localizedName = useLocalizedName();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-primary-100 flex items-center justify-center p-4 md:p-8">
|
||||
<div className="max-w-6xl w-full">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||
{localizedName(item.messageTitle) || t("Leadership Message")}
|
||||
</h1>
|
||||
<div className="w-24 h-1 bg-primary-600 mx-auto"></div>
|
||||
<p className="text-gray-600 mt-4 max-w-2xl mx-auto">
|
||||
{t("Message from our leadership")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Message Card */}
|
||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Image Section with overlay */}
|
||||
<div className="md:w-2/5 relative">
|
||||
<img
|
||||
src={item.presigned || "/placeholder.jpg"}
|
||||
alt={localizedName(item.messageTitle) || "Leadership"}
|
||||
className="w-full h-64 md:h-full object-cover transition-opacity duration-500"
|
||||
/>
|
||||
|
||||
{/* Overlay with name & position */}
|
||||
{/* <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent flex items-end p-6 md:p-8">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-2">
|
||||
{localizedName(item.employeeName) || t("Office Head")}
|
||||
</h2>
|
||||
<p className="text-gray-200 text-lg">
|
||||
{localizedName(item.employeePosition) || ""}
|
||||
</p>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Message Content Section */}
|
||||
<div className="md:w-3/5 p-6 md:p-8 flex flex-col justify-center">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="w-10 h-1 bg-primary-600 mr-3"></div>
|
||||
<h3 className="text-lg font-semibold text-primary-600 uppercase tracking-wide">
|
||||
{localizedName(item.messageTitle) ||
|
||||
t("Leadership Message")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Localized Message Content */}
|
||||
<div className="min-h-[200px] flex items-center">
|
||||
<p className="text-gray-700 text-lg leading-relaxed">
|
||||
{localizedName(item.messageContent) ||
|
||||
t("No content available")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Section */}
|
||||
<div className="mt-8 flex justify-end items-center gap-3 border-t pt-4">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{t("Status")}:
|
||||
</span>
|
||||
<span
|
||||
className={`px-4 py-2 text-sm font-semibold rounded-full ${
|
||||
item.isActive
|
||||
? "bg-primary-100 text-primary-700"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}`}>
|
||||
{item.isActive ? t("Active") : t("Inactive")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import React from "react";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MessageItem } from "../types/messageTypes";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import { useMessage } from "../hooks/useMessage";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export const ViewMessage = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { singleItem: item } = useMessage(undefined, id);
|
||||
const locale = i18n.language;
|
||||
const localizedName = useLocalizedName();
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4 md:p-8">
|
||||
<div className="max-w-6xl w-full">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||
{t("Leadership Message")}
|
||||
</h1>
|
||||
<div className="w-24 h-1 bg-indigo-600 mx-auto"></div>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Image Section */}
|
||||
<div className="md:w-2/5 relative">
|
||||
{item?.presigned ? (
|
||||
<img
|
||||
src={item.presigned}
|
||||
alt={localizedName(item.messageTitle)}
|
||||
className="w-full h-64 md:h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-64 md:h-full flex items-center justify-center bg-gray-100 text-gray-400">
|
||||
<ImageIcon className="h-10 w-10" />
|
||||
<span className="ml-2">{t("No image available")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay with title */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent flex items-end p-6 md:p-8">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-2">
|
||||
{localizedName(item?.messageTitle)}
|
||||
</h2>
|
||||
<p className="text-gray-200 text-lg">
|
||||
{t("Office Leadership")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkmark */}
|
||||
<div className="absolute top-6 right-6 bg-white text-indigo-600 rounded-full p-2 shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Section */}
|
||||
<div className="md:w-3/5 p-6 md:p-8 flex flex-col justify-center">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="w-10 h-1 bg-indigo-600 mr-3"></div>
|
||||
<h3 className="text-lg font-semibold text-indigo-600 uppercase tracking-wide">
|
||||
{t("Message Content")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto pr-4 custom-scrollbar">
|
||||
<p className="text-gray-700 text-lg leading-relaxed whitespace-pre-line">
|
||||
{localizedName(item?.messageContent)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Section */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
{/* Status Toggle */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{t("Status")}
|
||||
</span>
|
||||
<div
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
item?.isActive ? "bg-primary-500" : "bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
item?.isActive ? "translate-x-6" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
item?.isActive ? "text-primary-600" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{item?.isActive ? t("Active") : t("Inactive")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
{item?.createdAt && (
|
||||
<span className="text-sm text-gray-500">
|
||||
{locale === "en"
|
||||
? new Date(item.createdAt).toLocaleString(locale, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: item.amharicCreatedAt}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user