Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 17 additions & 42 deletions apps/marketing/src/components/ComparisonLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React, { ReactNode, useState, useEffect, useMemo } from "react";
import NextLink from "next/link";
import { Typography, Table, Space, Card, Button, Tooltip, Alert } from "antd";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import PageWrapper from "@/components/PageWrapper";
import { ALL_FEATURES, getFeatureStatus } from "@/constants/comparisonFeatures";
import { FEATURE_LABELS } from "@/constants/featureLabels";
import { FeatureStatusBadge } from "@/components/CommonFeatures";
import { SIMPLE_TABLE_MULTI_FRAMEWORK_TAGLINE } from "@/constants/frameworkIntegrationHub";
import { SIMPLE_TABLE_INFO } from "@/constants/packageInfo";

const { Title, Paragraph, Text, Link } = Typography;
Expand All @@ -25,8 +23,6 @@ interface ComparisonLayoutProps {
competitorPackage: string; // Package key like "agGrid"
performanceMetrics: PerformanceMetricsProps;
summaryContent: ReactNode;
/** When true, shows a short note that Simple Table supports multiple frameworks (default: true). */
showFrameworksCallout?: boolean;
}

const ComparisonLayout: React.FC<ComparisonLayoutProps> = ({
Expand All @@ -37,7 +33,6 @@ const ComparisonLayout: React.FC<ComparisonLayoutProps> = ({
competitorPackage,
performanceMetrics,
summaryContent,
showFrameworksCallout = true,
}) => {
const [isMobile, setIsMobile] = useState(false);

Expand Down Expand Up @@ -115,7 +110,7 @@ const ComparisonLayout: React.FC<ComparisonLayoutProps> = ({
align: "center" as const,
},
],
[competitorName, isMobile]
[competitorName, isMobile],
);

return (
Expand All @@ -138,52 +133,32 @@ const ComparisonLayout: React.FC<ComparisonLayoutProps> = ({
</Paragraph>
</div>

{showFrameworksCallout ? (
{/* Introduction */}
<div className="mb-8 text-center">
<Paragraph className="text-lg text-gray-700 dark:text-gray-300 mb-4">
{introText}
</Paragraph>
</div>

{/* AI Disclaimer */}
<div className="mb-8">
<Alert
message="Multi-framework data grid"
message="AI-Assisted Content"
description={
<Text>
{SIMPLE_TABLE_MULTI_FRAMEWORK_TAGLINE}{" "}
<NextLink
href="/frameworks"
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
Browse framework setup
</NextLink>
.
This comparison guide was created with AI assistance. While we strive for accuracy,
if you notice any incorrect information, please{" "}
<Link href="mailto:peter@peteryng.com" strong>
contact us
</Link>{" "}
so we can correct it promptly.
</Text>
}
type="info"
showIcon
className="mb-8 max-w-3xl mx-auto"
/>
) : null}

{/* Introduction */}
<div className="mb-8 text-center">
<Paragraph className="text-lg text-gray-700 dark:text-gray-300 mb-4">
{introText}
</Paragraph>
</div>

{/* AI Disclaimer */}
<Alert
message="AI-Assisted Content"
description={
<Text>
This comparison guide was created with AI assistance. While we strive for accuracy, if
you notice any incorrect information, please{" "}
<Link href="mailto:peter@peteryng.com" strong>
contact us
</Link>{" "}
so we can correct it promptly.
</Text>
}
type="info"
showIcon
className="mb-8"
/>

{/* Comparison Table */}
<Card
className="mb-8 shadow-sm dark:bg-gray-800 dark:border-gray-700"
Expand Down
176 changes: 121 additions & 55 deletions apps/marketing/src/examples/crm/CRMFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { useSyncExternalStore } from "react";
import type { FooterRendererProps } from "@simple-table/react";

const MOBILE_MQ = "(max-width: 740px)";

function getMobileMq(): MediaQueryList | null {
if (typeof window === "undefined") return null;
return window.matchMedia(MOBILE_MQ);
}

function subscribeMobile(cb: () => void) {
const mq = getMobileMq();
if (!mq) return () => {};
mq.addEventListener("change", cb);
return () => mq.removeEventListener("change", cb);
}

function getMobileSnapshot(): boolean {
return getMobileMq()?.matches ?? false;
}

function useFooterIsMobile(): boolean {
return useSyncExternalStore(subscribeMobile, getMobileSnapshot, () => false);
}

const CRMCustomFooter = ({
currentPage,
totalPages,
Expand All @@ -15,26 +38,25 @@ const CRMCustomFooter = ({
isDark,
setRowsPerPage,
}: FooterRendererProps & { isDark?: boolean; setRowsPerPage: (rowsPerPage: number) => void }) => {
// Generate visible page numbers with current page centered
const isMobile = useFooterIsMobile();

const generateVisiblePages = (currentPage: number, totalPages: number): number[] => {
const maxVisible = 5;

if (totalPages <= maxVisible) {
// Show all pages if we have fewer than maxVisible
return Array.from({ length: totalPages }, (_, i) => i + 1);
}

// Calculate start and end to center current page
let start = currentPage - 2;
let end = currentPage + 2;
const halfLeft = Math.floor((maxVisible - 1) / 2);
const halfRight = Math.ceil((maxVisible - 1) / 2);
let start = currentPage - halfLeft;
let end = currentPage + halfRight;

// Adjust if we're near the beginning
if (start < 1) {
start = 1;
end = Math.min(maxVisible, totalPages);
}

// Adjust if we're near the end
if (end > totalPages) {
end = totalPages;
start = Math.max(1, totalPages - maxVisible + 1);
Expand All @@ -43,7 +65,7 @@ const CRMCustomFooter = ({
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
};

const visiblePages = generateVisiblePages(currentPage, totalPages);
const visiblePages = isMobile ? [] : generateVisiblePages(currentPage, totalPages);

const colors = isDark
? {
Expand Down Expand Up @@ -73,78 +95,102 @@ const CRMCustomFooter = ({
activeText: "#ea580c",
};

const summaryFontSize = isMobile ? "12px" : "14px";
const controlFontSize = isMobile ? "12px" : "14px";
const pageBtnPadding = isMobile ? "6px 10px" : "8px 16px";
const arrowPadding = isMobile ? "6px" : "8px";

const selectStyle = {
border: `1px solid ${colors.inputBorder}`,
borderRadius: "6px",
padding: isMobile ? "2px 6px" : "4px 8px",
fontSize: controlFontSize,
backgroundColor: colors.inputBg,
color: colors.text,
cursor: "pointer" as const,
maxWidth: isMobile ? "4.5rem" : undefined,
};

return (
<div
className="crm-footer"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderTop: `1px solid ${colors.border}`,
backgroundColor: colors.bg,
}}
>
{/* Results text */}
<p style={{ fontSize: "14px", color: colors.text, margin: 0 }}>
Showing <span style={{ fontWeight: "500" }}>{startRow}</span> to{" "}
<span style={{ fontWeight: "500" }}>{endRow}</span> of{" "}
<span style={{ fontWeight: "500" }}>{totalRows}</span> results
<p
className="crm-footer__summary"
style={{ fontSize: summaryFontSize, color: colors.text, margin: 0, whiteSpace: "nowrap" }}
>
{isMobile ? (
<>
<span style={{ fontWeight: 500 }}>{startRow}</span>–
<span style={{ fontWeight: 500 }}>{endRow}</span>
{" / "}
<span style={{ fontWeight: 500 }}>{totalRows}</span>
</>
) : (
<>
Showing <span style={{ fontWeight: "500" }}>{startRow}</span> to{" "}
<span style={{ fontWeight: "500" }}>{endRow}</span> of{" "}
<span style={{ fontWeight: "500" }}>{totalRows}</span> results
</>
)}
</p>

{/* Controls */}
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
{/* Page size selector */}
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<label htmlFor="itemsPerPage" style={{ fontSize: "14px", color: colors.text }}>
Show:
</label>
<div className="crm-footer__toolbar">
<div
className="crm-footer__page-size"
style={{ display: "flex", alignItems: "center", gap: isMobile ? 0 : "8px" }}
>
{!isMobile && (
<label htmlFor="itemsPerPage" style={{ fontSize: "14px", color: colors.text }}>
Show:
</label>
)}
<select
id="itemsPerPage"
aria-label={isMobile ? "Rows per page" : undefined}
value={rowsPerPage}
onChange={(event) => {
setRowsPerPage(parseInt(event.target.value, 10));
onPageChange(1);
}}
style={{
border: `1px solid ${colors.inputBorder}`,
borderRadius: "6px",
padding: "4px 8px",
fontSize: "14px",
backgroundColor: colors.inputBg,
color: colors.text,
cursor: "pointer",
}}
style={selectStyle}
>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="10000">all</option>
</select>
<span style={{ fontSize: "14px", color: colors.text }}>per page</span>
{!isMobile && <span style={{ fontSize: "14px", color: colors.text }}>per page</span>}
</div>

{/* Pagination */}
<nav
className="crm-footer__pagination"
style={{
display: "inline-flex",
borderRadius: "6px",
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
flexShrink: 0,
alignItems: "center",
}}
>
{/* Previous button */}
<button
type="button"
onClick={onPrevPage}
disabled={!hasPrevPage}
style={{
display: "inline-flex",
alignItems: "center",
padding: "8px",
padding: arrowPadding,
borderTopLeftRadius: "6px",
borderBottomLeftRadius: "6px",
border: `1px solid ${colors.buttonBorder}`,
backgroundColor: colors.buttonBg,
fontSize: "14px",
fontSize: controlFontSize,
fontWeight: "500",
color: colors.buttonText,
cursor: hasPrevPage ? "pointer" : "not-allowed",
Expand All @@ -154,41 +200,61 @@ const CRMCustomFooter = ({
</button>

{/* Page buttons */}
{visiblePages.map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
{isMobile ? (
<span
style={{
display: "inline-flex",
alignItems: "center",
padding: "8px 16px",
justifyContent: "center",
padding: pageBtnPadding,
border: `1px solid ${colors.buttonBorder}`,
backgroundColor: currentPage === page ? colors.activeBg : colors.buttonBg,
fontSize: "14px",
fontWeight: "500",
color: currentPage === page ? colors.activeText : colors.text,
cursor: "pointer",
backgroundColor: colors.activeBg,
fontSize: controlFontSize,
fontWeight: 600,
color: colors.activeText,
marginLeft: "-1px",
minWidth: "2.75rem",
}}
>
{page}
</button>
))}
{currentPage}/{Math.max(totalPages, 1)}
</span>
) : (
visiblePages.map((page) => (
<button
type="button"
key={page}
onClick={() => onPageChange(page)}
style={{
display: "inline-flex",
alignItems: "center",
padding: pageBtnPadding,
border: `1px solid ${colors.buttonBorder}`,
backgroundColor: currentPage === page ? colors.activeBg : colors.buttonBg,
fontSize: controlFontSize,
fontWeight: "500",
color: currentPage === page ? colors.activeText : colors.text,
cursor: "pointer",
marginLeft: "-1px",
}}
>
{page}
</button>
))
)}

{/* Next button */}
<button
type="button"
onClick={onNextPage}
disabled={!hasNextPage}
style={{
display: "inline-flex",
alignItems: "center",
padding: "8px",
padding: arrowPadding,
borderTopRightRadius: "6px",
borderBottomRightRadius: "6px",
border: `1px solid ${colors.buttonBorder}`,
backgroundColor: colors.buttonBg,
fontSize: "14px",
fontSize: controlFontSize,
fontWeight: "500",
color: colors.buttonText,
cursor: hasNextPage ? "pointer" : "not-allowed",
Expand Down
Loading
Loading