mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Merge branch 'freight_feature/priority' of github.com:Tria-plc/edr-platform into freight_feature/priority
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
import { FilesService } from '../files/files.service';
|
import { FilesService } from '../files/files.service';
|
||||||
|
import { FileRecord } from '../files/entities/file.entity';
|
||||||
import { MinioService } from '../minio/minio.service';
|
import { MinioService } from '../minio/minio.service';
|
||||||
import { SignaturesRepository } from './signatures.repository';
|
import { SignaturesRepository } from './signatures.repository';
|
||||||
import { SavedSignature } from './entities/saved-signature.entity';
|
import { SavedSignature } from './entities/saved-signature.entity';
|
||||||
@@ -19,6 +21,7 @@ export class SignaturesService {
|
|||||||
private readonly signaturesRepository: SignaturesRepository,
|
private readonly signaturesRepository: SignaturesRepository,
|
||||||
private readonly filesService: FilesService,
|
private readonly filesService: FilesService,
|
||||||
private readonly minioService: MinioService,
|
private readonly minioService: MinioService,
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Saved signature for a user, with the image inlined as a data URL (or null). */
|
/** Saved signature for a user, with the image inlined as a data URL (or null). */
|
||||||
@@ -47,18 +50,32 @@ export class SignaturesService {
|
|||||||
path: '',
|
path: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileRecord = await this.filesService.upsertByCode({
|
// Capture the previously referenced file so we can remove it only AFTER the
|
||||||
|
// saved_signatures row is repointed — deleting it first would violate the
|
||||||
|
// FK constraint (saved_signatures.signature_file_id -> files.id).
|
||||||
|
const existing = await this.signaturesRepository.findByUserId(input.userId);
|
||||||
|
const previousFileId = existing?.signatureFileId ?? null;
|
||||||
|
|
||||||
|
const fileRecord = await this.filesService.upload({
|
||||||
resourceId: input.userId,
|
resourceId: input.userId,
|
||||||
resource: 'saved_signatures',
|
resource: 'saved_signatures',
|
||||||
code: 'signature',
|
code: 'signature',
|
||||||
file,
|
file,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.signaturesRepository.upsert({
|
const saved = await this.signaturesRepository.upsert({
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
signerDisplayName: input.signerDisplayName,
|
signerDisplayName: input.signerDisplayName,
|
||||||
signatureFileId: fileRecord.id,
|
signatureFileId: fileRecord.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (previousFileId && previousFileId !== fileRecord.id) {
|
||||||
|
await this.dataSource
|
||||||
|
.getRepository(FileRecord)
|
||||||
|
.delete({ id: previousFileId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async inlineImageUrl(
|
private async inlineImageUrl(
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Building2,
|
||||||
|
FileCheck,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Phone,
|
||||||
|
User,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Group, Stack, Text, Divider } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { BookingDetail } from "@/types/booking";
|
||||||
|
import { SectionCard } from "./SectionCard";
|
||||||
|
|
||||||
|
interface InfoRowProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
value?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoRow({ icon: Icon, label, value }: InfoRowProps) {
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" wrap="nowrap" py={6} gap="md">
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<Icon size={15} color="var(--mantine-color-gray-5)" />
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" fw={600} ta="right" style={{ minWidth: 0 }} truncate>
|
||||||
|
{value || "—"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookingCompanyCardProps {
|
||||||
|
booking: BookingDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Customer (company) information for the booking. */
|
||||||
|
export function BookingCompanyCard({ booking }: BookingCompanyCardProps) {
|
||||||
|
const company = booking.company;
|
||||||
|
|
||||||
|
// Government bookings may not carry a company; show the institution instead.
|
||||||
|
if (!company && booking.isGovernment) {
|
||||||
|
return (
|
||||||
|
<SectionCard icon={Building2} title="Customer" accent="blue">
|
||||||
|
<InfoRow
|
||||||
|
icon={Building2}
|
||||||
|
label="Government"
|
||||||
|
value={booking.governmentInstitution}
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
return (
|
||||||
|
<SectionCard icon={Building2} title="Customer" accent="blue">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
No customer linked to this booking.
|
||||||
|
</Text>
|
||||||
|
</SectionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyName = company.companyName ?? company.name ?? company.label;
|
||||||
|
|
||||||
|
const rows: InfoRowProps[] = [
|
||||||
|
{ icon: FileCheck, label: "TIN", value: company.tin },
|
||||||
|
{ icon: Mail, label: "Email", value: company.email },
|
||||||
|
{ icon: Phone, label: "Phone", value: company.phone },
|
||||||
|
{ icon: MapPin, label: "Address", value: company.address },
|
||||||
|
{ icon: User, label: "Contact person", value: company.contactPersonName },
|
||||||
|
{ icon: Phone, label: "Contact phone", value: company.contactPersonPhone },
|
||||||
|
].filter((r) => r.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCard
|
||||||
|
icon={Building2}
|
||||||
|
title="Customer"
|
||||||
|
subtitle={companyName}
|
||||||
|
accent="blue"
|
||||||
|
>
|
||||||
|
<Stack gap={0}>
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
No additional company details available.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
rows.map((row, index) => (
|
||||||
|
<div key={row.label}>
|
||||||
|
{index > 0 && <Divider color="var(--mantine-color-gray-2)" />}
|
||||||
|
<InfoRow {...row} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</SectionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,3 +17,4 @@ export * from "./BookingRouteServiceCard";
|
|||||||
export * from "./BookingMileServicesCard";
|
export * from "./BookingMileServicesCard";
|
||||||
export * from "./BookingCargoCard";
|
export * from "./BookingCargoCard";
|
||||||
export * from "./BookingContractSummaryCard";
|
export * from "./BookingContractSummaryCard";
|
||||||
|
export * from "./BookingCompanyCard";
|
||||||
|
|||||||
@@ -50,11 +50,8 @@ export default function BookingContractPage() {
|
|||||||
enabled: Boolean(id),
|
enabled: Boolean(id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const signRole: "CUSTOMER" | "STAFF" | null = data?.canSignCustomer
|
// Backoffice only ever signs as STAFF — customers sign in the portal.
|
||||||
? "CUSTOMER"
|
const canSign = Boolean(data?.canSignStaff);
|
||||||
: data?.canSignStaff
|
|
||||||
? "STAFF"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const savedSignature = data?.savedSignature ?? null;
|
const savedSignature = data?.savedSignature ?? null;
|
||||||
const savedSignatureImage = savedSignature?.signatureImageUrl ?? null;
|
const savedSignatureImage = savedSignature?.signatureImageUrl ?? null;
|
||||||
@@ -106,12 +103,12 @@ export default function BookingContractPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmSign = () => {
|
const confirmSign = () => {
|
||||||
if (!signRole || !signerName.trim()) return;
|
if (!canSign || !signerName.trim()) return;
|
||||||
// Approve the saved signature, or submit the freshly drawn one.
|
// Approve the saved signature, or submit the freshly drawn one.
|
||||||
const image = usingSaved ? savedSignatureImage : signatureData;
|
const image = usingSaved ? savedSignatureImage : signatureData;
|
||||||
if (!image) return;
|
if (!image) return;
|
||||||
signMutation.mutate({
|
signMutation.mutate({
|
||||||
role: signRole,
|
role: "STAFF",
|
||||||
signatureImageBase64: image,
|
signatureImageBase64: image,
|
||||||
signerDisplayName: signerName.trim(),
|
signerDisplayName: signerName.trim(),
|
||||||
consentText: "I agree to the terms of this contract.",
|
consentText: "I agree to the terms of this contract.",
|
||||||
@@ -167,10 +164,10 @@ export default function BookingContractPage() {
|
|||||||
<Download className="size-4" />
|
<Download className="size-4" />
|
||||||
Download PDF
|
Download PDF
|
||||||
</Button>
|
</Button>
|
||||||
{signRole && (
|
{canSign && (
|
||||||
<Button size="sm" className="gap-2" onClick={openSign}>
|
<Button size="sm" className="gap-2" onClick={openSign}>
|
||||||
<FileSignature className="size-4" />
|
<FileSignature className="size-4" />
|
||||||
Sign as {signRole === "CUSTOMER" ? "Customer" : "Staff"}
|
{usingSaved ? "Approve & sign" : "Sign contract"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -194,9 +191,7 @@ export default function BookingContractPage() {
|
|||||||
<Dialog open={signOpen} onOpenChange={setSignOpen}>
|
<Dialog open={signOpen} onOpenChange={setSignOpen}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>Staff signature</DialogTitle>
|
||||||
{signRole === "CUSTOMER" ? "Customer signature" : "Staff signature"}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{usingSaved
|
{usingSaved
|
||||||
? `Review your saved signature and approve it to execute the contract for ${data.reference}.`
|
? `Review your saved signature and approve it to execute the contract for ${data.reference}.`
|
||||||
|
|||||||
@@ -24,11 +24,16 @@ import {
|
|||||||
BookingRouteServiceCard,
|
BookingRouteServiceCard,
|
||||||
BookingMileServicesCard,
|
BookingMileServicesCard,
|
||||||
BookingCargoCard,
|
BookingCargoCard,
|
||||||
|
BookingCompanyCard,
|
||||||
BookingContractSummaryCard,
|
BookingContractSummaryCard,
|
||||||
|
BookingDocumentsCard,
|
||||||
|
type BookingFileView,
|
||||||
} from "@/components/bookings/detail";
|
} from "@/components/bookings/detail";
|
||||||
import { getStatusMeta } from "@/features/bookings/booking-status.config";
|
import { getStatusMeta } from "@/features/bookings/booking-status.config";
|
||||||
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
|
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
|
||||||
|
import { downloadBookingFile } from "@/services/files.service";
|
||||||
import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings";
|
import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export default function BookingRequestDetailPage() {
|
export default function BookingRequestDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -36,6 +41,14 @@ export default function BookingRequestDetailPage() {
|
|||||||
const { data: booking, isLoading, isError, refetch, isFetching } = useBookingDetail(id);
|
const { data: booking, isLoading, isError, refetch, isFetching } = useBookingDetail(id);
|
||||||
const mutations = useBookingMutations(id ?? "");
|
const mutations = useBookingMutations(id ?? "");
|
||||||
|
|
||||||
|
const handleDownloadFile = async (file: BookingFileView) => {
|
||||||
|
try {
|
||||||
|
await downloadBookingFile(file.id, file.name);
|
||||||
|
} catch {
|
||||||
|
toast.error("Could not download file.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box style={detailStyles.page}>
|
<Box style={detailStyles.page}>
|
||||||
@@ -147,6 +160,10 @@ export default function BookingRequestDetailPage() {
|
|||||||
{booking.contractSummary && (
|
{booking.contractSummary && (
|
||||||
<BookingContractSummaryCard summary={booking.contractSummary} />
|
<BookingContractSummaryCard summary={booking.contractSummary} />
|
||||||
)}
|
)}
|
||||||
|
<BookingDocumentsCard
|
||||||
|
files={booking.files ?? []}
|
||||||
|
onDownload={handleDownloadFile}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
@@ -154,6 +171,7 @@ export default function BookingRequestDetailPage() {
|
|||||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
<Box style={{ position: "sticky", top: 24 }}>
|
<Box style={{ position: "sticky", top: 24 }}>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
|
<BookingCompanyCard booking={booking} />
|
||||||
<BookingPricingSummary booking={booking} />
|
<BookingPricingSummary booking={booking} />
|
||||||
<BookingActionsToolbar booking={booking} mutations={mutations} />
|
<BookingActionsToolbar booking={booking} mutations={mutations} />
|
||||||
{showContractButton && (
|
{showContractButton && (
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { api as client } from "../auth/http";
|
||||||
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||||
|
|
||||||
|
const F = URL_CONSTANTS.FILES;
|
||||||
|
|
||||||
|
export const filesService = {
|
||||||
|
/** Stream a stored file by id (backend route: GET /files/:id). */
|
||||||
|
download: async (id: string): Promise<Blob> => {
|
||||||
|
const response = await client.get(F.BY_ID(id), { responseType: "blob" });
|
||||||
|
return response.data as Blob;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Download a file blob and trigger a browser save with the given name. */
|
||||||
|
export async function downloadBookingFile(
|
||||||
|
id: string,
|
||||||
|
filename: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const blob = await filesService.download(id);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
@@ -30,6 +30,27 @@ export interface BookingNamedRef {
|
|||||||
companyName?: string;
|
companyName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Full company record the booking response joins in (subset used by the UI). */
|
||||||
|
export interface BookingCompany {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
status?: string;
|
||||||
|
tin?: string | null;
|
||||||
|
vatNumber?: string | null;
|
||||||
|
businessLicense?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
contactPersonName?: string | null;
|
||||||
|
contactPersonPhone?: string | null;
|
||||||
|
generalManagerName?: string | null;
|
||||||
|
generalManagerEmail?: string | null;
|
||||||
|
generalManagerPhone?: string | null;
|
||||||
|
website?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BookingContainerLine {
|
export interface BookingContainerLine {
|
||||||
id: string;
|
id: string;
|
||||||
containerTypeId: string;
|
containerTypeId: string;
|
||||||
@@ -81,6 +102,8 @@ export interface BookingFile {
|
|||||||
name: string;
|
name: string;
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
|
url?: string;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookingDetail {
|
export interface BookingDetail {
|
||||||
@@ -121,7 +144,7 @@ export interface BookingDetail {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
// customer?: BookingNamedRef & { companyName?: string };
|
// customer?: BookingNamedRef & { companyName?: string };
|
||||||
company?: BookingNamedRef;
|
company?: BookingNamedRef & Partial<BookingCompany>;
|
||||||
originYard?: BookingNamedRef;
|
originYard?: BookingNamedRef;
|
||||||
destinationYard?: BookingNamedRef;
|
destinationYard?: BookingNamedRef;
|
||||||
serviceType?: BookingNamedRef & { code?: string; priorityBonusPoints?: number };
|
serviceType?: BookingNamedRef & { code?: string; priorityBonusPoints?: number };
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import useAuth from "./hooks/useAuth";
|
|||||||
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
|
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
|
||||||
import MyPortalPage from "./pages/MyPortalPage";
|
import MyPortalPage from "./pages/MyPortalPage";
|
||||||
import ProfilePage from "./pages/ProfilePage";
|
import ProfilePage from "./pages/ProfilePage";
|
||||||
|
import MySignaturePage from "./pages/MySignaturePage";
|
||||||
import SettingsPage from "./pages/SettingsPage";
|
import SettingsPage from "./pages/SettingsPage";
|
||||||
import LoginPage from "./pages/accounts/LoginPage";
|
import LoginPage from "./pages/accounts/LoginPage";
|
||||||
import OnboardingPage from "./pages/accounts/OnboardingPage";
|
import OnboardingPage from "./pages/accounts/OnboardingPage";
|
||||||
@@ -201,6 +202,7 @@ const App = () => {
|
|||||||
<Route path="/tracking" element={<TrackingPage />} />
|
<Route path="/tracking" element={<TrackingPage />} />
|
||||||
<Route path="/billing" element={<BillingPage />} />
|
<Route path="/billing" element={<BillingPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
<Route path="/signature" element={<MySignaturePage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export function AppLayout({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<FileSignature size={15} />}
|
leftSection={<FileSignature size={15} />}
|
||||||
onClick={() => navigate("/profile#signature")}
|
onClick={() => navigate("/signature")}
|
||||||
>
|
>
|
||||||
My signature
|
My signature
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||||
|
export const API_BASE_URL = 'http://localhost:3001';
|
||||||
|
|
||||||
|
|||||||
19
apps/edr-freight-web/portal/src/pages/MySignaturePage.tsx
Normal file
19
apps/edr-freight-web/portal/src/pages/MySignaturePage.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { MySignatureCard } from "@/components/profile/MySignatureCard";
|
||||||
|
|
||||||
|
export default function MySignaturePage() {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-8">
|
||||||
|
<div className="mx-auto flex max-w-md flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black tracking-tight text-foreground">
|
||||||
|
My signature
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Saved and reused to approve and sign booking contracts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<MySignatureCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { User, Building2, Phone, Mail, MapPin, ShieldCheck, Briefcase, UserCheck, Fingerprint, FileCheck, Globe, Building } from "lucide-react";
|
import { User, Building2, Phone, Mail, MapPin, ShieldCheck, Briefcase, UserCheck, Fingerprint, FileCheck, Globe, Building } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { MySignatureCard } from "@/components/profile/MySignatureCard";
|
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Separator } from "@edr/ui-common";
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Separator } from "@edr/ui-common";
|
||||||
|
|
||||||
function InfoItem({ icon, label, value }: { icon?: React.ReactNode; label: string; value?: string | null }) {
|
function InfoItem({ icon, label, value }: { icon?: React.ReactNode; label: string; value?: string | null }) {
|
||||||
@@ -176,10 +175,6 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div id="signature">
|
|
||||||
<MySignatureCard />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user