diff --git a/.gitignore b/.gitignore index 320c064..6c0dd06 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,6 @@ dist # Gatsby files .cache/ -public # Storybook build outputs .out diff --git a/README.md b/README.md index 17136cb..fbcc8b8 100644 --- a/README.md +++ b/README.md @@ -22,58 +22,91 @@ AppSutra is a community-driven directory of SaaS products optimized for Indian b ### Adding Your Product 1. **Fork this repository** -2. **Create your listing file** at `listings//.md` +2. **Create your listing file** at `listings//.md` 3. **Fill in the details** using our template (see example below) 4. **Open a Pull Request** - our validation will run automatically 5. **Get reviewed** by maintainers and go live! +> **Note:** Slugs are auto-generated from your `name` and `category` fields in YAML. The filename must match `slugify(name)` and the folder must match `slugify(category)`. Validation runs automatically on PR submission. + ### Example Listing Structure ```yaml --- -name: "Zoho CRM" -slug: "zoho-crm" -category: "crm" -website: "https://www.zoho.com/crm/" -logo: "https://example.com/logo.png" -pricing: "Free + Paid from โ‚น900/mo" -locations: ["India", "Global"] -use_cases: ["SMB", "Sales Automation", "Lead Management"] -keywords: ["crm", "sales", "automation"] -trial: true -integrations: ["Gmail", "WhatsApp", "Zapier"] -contact_email: "partners@zoho.com" -listing_owner_github: "@zohocrm" -verified: false -updated_at: "2025-09-15" +icon: "https://example.com/logo.png" +name: "Razorpay" +company: "Razorpay Software Private Limited" +trialPlan: true +trialPlanPricing: "Free" +category: "Finance" +# IMPORTANT: Do NOT add 'slug' or 'categorySlug' fields +# They are auto-generated during validation and build from 'name' and 'category' +# - Filename must be: slugify("Razorpay") = "razorpay.md" +# - Folder must be: slugify("Finance") = "finance/" +# - Location: listings/finance/razorpay.md +useCases: + - "Online Payments" + - "E-commerce" + - "Subscription Billing" +keywords: + - "payment gateway" + - "online payments" + - "e-commerce" +integration: + - title: "Shopify" + - title: "WooCommerce" + - title: "Zapier" +description: | + Razorpay is a comprehensive payment gateway platform designed to empower businesses... + + **Key Highlights** + - Comprehensive payment processing platform + - Accept payments globally through multiple channels + - Simple API integrations for developers +locations: + - "India" + - "Global" +website: "https://razorpay.com/" +updated_at: "2025-01-18" +keyFeatures: + description: "Razorpay offers a full range of payment tools..." + features: + - title: "Payment Gateway" + desc: "Accept payments via credit cards, debit cards, UPI, wallets." + - title: "Subscription Management" + desc: "Create and manage recurring billing for subscription-based businesses." + # Maximum 8 features +buyingGuide: + - question: "1. What's your business model?" + why: "Helps determine the payment features you need" + answer: "E-commerce, subscription-based, service-based, or other?" + # Maximum 8 questions +pricing: + desc: "Razorpay offers flexible pricing plans..." + plans: + - name: "Basic Plan" + pricing: + amount: 0 + currency: "INR" + currencySymbol: "โ‚น" + period: "month" + perUnit: null + description: "Free plan with all essential features." + # Maximum 4 plans --- - -**About** - -Zoho CRM is a comprehensive customer relationship management platform designed for businesses of all sizes... - -**Highlights** -- Advanced pipeline management -- WhatsApp integration for India -- Mobile apps for field sales - -**Pricing** -- Free: Up to 3 users -- Standard: โ‚น900/user/month -- Professional: โ‚น1,600/user/month ``` ## ๐Ÿ“‚ Categories - **CRM** - Customer relationship management -- **HR** - Human resources and recruitment +- **Human Resource** - HR software and recruitment +- **Payment Gateway** - Payment processing and merchant services - **Marketing** - Digital marketing and automation -- **Finance** - Accounting and financial management +- **Finance** - Financial management tools - **Support** - Customer support and helpdesk - **Analytics** - Business intelligence and analytics - **Security** - Cybersecurity and compliance - **DevTools** - Development and technical tools -- **Accounting** - Accounting and bookkeeping ## ๐Ÿ” Writing Reviews @@ -114,20 +147,83 @@ npm run linkcheck npm test ``` +### Frontend Development + +```bash +# Navigate to frontend directory +cd frontend + +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build + +# The build process will: +# 1. Read all .md files from ../listings/ +# 2. Auto-generate slugs from name and category +# 3. Generate static pages for each product +# 4. Output optimized HTML files +``` + ### File Structure ``` appsutra/ -โ”œโ”€โ”€ listings/ # Product listings organized by category -โ”‚ โ”œโ”€โ”€ crm/ -โ”‚ โ”œโ”€โ”€ hr/ -โ”‚ โ””โ”€โ”€ ... -โ”œโ”€โ”€ schema/ # JSON Schema validation -โ”œโ”€โ”€ scripts/ # Validation and utility scripts -โ”œโ”€โ”€ .github/ # GitHub templates and workflows -โ””โ”€โ”€ docs/ # Documentation +โ”œโ”€โ”€ listings/ # Product listings (organized by category) +โ”‚ โ”œโ”€โ”€ hr/ # HR category folder +โ”‚ โ”‚ โ””โ”€โ”€ keka-services.md +โ”‚ โ”œโ”€โ”€ finance/ # Finance category folder +โ”‚ โ”‚ โ””โ”€โ”€ razorpay.md +โ”‚ โ””โ”€โ”€ payment-gateway/ # Payment Gateway category folder +โ”‚ โ””โ”€โ”€ ... +โ”œโ”€โ”€ frontend/ # Next.js web application +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ”œโ”€โ”€ app/ # Next.js App Router pages +โ”‚ โ”‚ โ”œโ”€โ”€ lib/ # Data parsing and utilities +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ listings.ts # Markdown parser & slug generator +โ”‚ โ”‚ โ”œโ”€โ”€ types/ # TypeScript type definitions +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ product.ts # Product type interface +โ”‚ โ”‚ โ””โ”€โ”€ components/ # React components +โ”‚ โ””โ”€โ”€ public/ # Static assets +โ”œโ”€โ”€ schema/ # JSON Schema validation +โ”œโ”€โ”€ scripts/ # Validation and utility scripts +โ”œโ”€โ”€ .github/ # GitHub templates and workflows +โ””โ”€โ”€ docs/ # Documentation ``` +### Architecture: Markdown โ†’ Static Site + +**How listings become web pages:** + +1. **Organize by Category**: Place product in category folder + - Example: `listings/finance/razorpay.md` + +2. **Define in YAML**: Add YAML front-matter with product details + - `name: "Razorpay"` + - `category: "Finance"` (display name, can differ from folder) + +3. **Auto-Generate Slugs**: Validation and build process generate slugs from YAML fields + - `name: "Razorpay"` โ†’ `slug: "razorpay"` (validated against filename) + - `category: "Finance"` โ†’ `categorySlug: "finance"` (validated against folder) + - **Validation ensures**: filename = slugify(name), folder = slugify(category) + +4. **Parse at Build Time**: `gray-matter` parses YAML โ†’ Product type object + +5. **Generate Static Pages**: Next.js creates HTML at `/finance/razorpay` + +6. **Deploy**: Static HTML files served via CDN + +**Key Points:** +- โœ… **Folder names** = Organizational structure (can be anything) +- โœ… **YAML category** = Source of truth for categorySlug +- โœ… **YAML name** = Source of truth for product slug +- โœ… No database required, fast static HTML +- โœ… Type-safe with TypeScript + ## ๐Ÿค Contributing We welcome contributions from the community! Please read our [Contributing Guide](CONTRIBUTING.md) for details on: @@ -140,10 +236,18 @@ We welcome contributions from the community! Please read our [Contributing Guide ## ๐Ÿ“œ Guidelines ### For Vendors -- Keep descriptions factual and neutral -- Include India-specific context where relevant -- Provide accurate pricing in โ‚น -- Maintain up-to-date information +- **Keep descriptions factual and neutral** - No promotional language +- **Include India-specific context** - โ‚น pricing, local integrations +- **Follow field limits**: + - Maximum 8 key features + - Maximum 4 pricing plans + - Maximum 8 buying guide questions + - Maximum 30 keywords +- **Use proper structure** - Follow the Product type interface exactly +- **No manual slugs** - Slugs are auto-generated from name and category +- **File naming**: Filename must match `slugify(name).md`, folder must match `slugify(category)` +- **Required fields**: All fields in Product schema are required, including `updated_at` (YYYY-MM-DD format) +- **Maintain up-to-date information** - Update `updated_at` when making changes ### For Users - Write constructive, helpful reviews diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..f826c54 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/package.json b/frontend/package.json index f2d491d..88fbdf0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,24 +9,32 @@ "lint": "next lint" }, "dependencies": { - "next": "15.5.3", - "react": "19.0.0", - "react-dom": "19.0.0", - "gray-matter": "^4.0.3", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.3", + "build": "^0.1.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "fuse.js": "^7.0.0", + "gray-matter": "^4.0.3", "lucide-react": "^0.468.0", - "clsx": "^2.1.1", - "tailwind-merge": "^2.5.4" + "next": "^15.5.5", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", + "autoprefixer": "^10", "eslint": "^9", "eslint-config-next": "15.5.3", "postcss": "^8", - "autoprefixer": "^10", "tailwindcss": "^3.4.1", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/frontend/public/icons/KeyFeatures/Calendar.png b/frontend/public/icons/KeyFeatures/Calendar.png new file mode 100644 index 0000000..ab68d08 Binary files /dev/null and b/frontend/public/icons/KeyFeatures/Calendar.png differ diff --git a/frontend/public/icons/KeyFeatures/Dollar.png b/frontend/public/icons/KeyFeatures/Dollar.png new file mode 100644 index 0000000..6e16e29 Binary files /dev/null and b/frontend/public/icons/KeyFeatures/Dollar.png differ diff --git a/frontend/public/icons/KeyFeatures/Progress.png b/frontend/public/icons/KeyFeatures/Progress.png new file mode 100644 index 0000000..4aa22c8 Binary files /dev/null and b/frontend/public/icons/KeyFeatures/Progress.png differ diff --git a/frontend/public/icons/KeyFeatures/Scale.png b/frontend/public/icons/KeyFeatures/Scale.png new file mode 100644 index 0000000..81bf18c Binary files /dev/null and b/frontend/public/icons/KeyFeatures/Scale.png differ diff --git a/frontend/public/icons/ProductDetailsPage/keka/icon.png b/frontend/public/icons/ProductDetailsPage/keka/icon.png new file mode 100644 index 0000000..a1fe552 Binary files /dev/null and b/frontend/public/icons/ProductDetailsPage/keka/icon.png differ diff --git a/frontend/public/icons/pricingOverview/Beginner.png b/frontend/public/icons/pricingOverview/Beginner.png new file mode 100644 index 0000000..c6e6998 Binary files /dev/null and b/frontend/public/icons/pricingOverview/Beginner.png differ diff --git a/frontend/public/icons/pricingOverview/Building.png b/frontend/public/icons/pricingOverview/Building.png new file mode 100644 index 0000000..c017cfe Binary files /dev/null and b/frontend/public/icons/pricingOverview/Building.png differ diff --git a/frontend/public/icons/pricingOverview/Increase.png b/frontend/public/icons/pricingOverview/Increase.png new file mode 100644 index 0000000..a517e1b Binary files /dev/null and b/frontend/public/icons/pricingOverview/Increase.png differ diff --git a/frontend/public/icons/pricingOverview/Scale.png b/frontend/public/icons/pricingOverview/Scale.png new file mode 100644 index 0000000..fbc248a Binary files /dev/null and b/frontend/public/icons/pricingOverview/Scale.png differ diff --git a/frontend/public/icons/productInfo/verifiedBadge.png b/frontend/public/icons/productInfo/verifiedBadge.png new file mode 100644 index 0000000..9f74c6a Binary files /dev/null and b/frontend/public/icons/productInfo/verifiedBadge.png differ diff --git a/frontend/public/icons/whyChooseUs/Discount.png b/frontend/public/icons/whyChooseUs/Discount.png new file mode 100644 index 0000000..cfaf9aa Binary files /dev/null and b/frontend/public/icons/whyChooseUs/Discount.png differ diff --git a/frontend/public/icons/whyChooseUs/LightningBolt.png b/frontend/public/icons/whyChooseUs/LightningBolt.png new file mode 100644 index 0000000..b6b0eb8 Binary files /dev/null and b/frontend/public/icons/whyChooseUs/LightningBolt.png differ diff --git a/frontend/public/icons/whyChooseUs/compare.png b/frontend/public/icons/whyChooseUs/compare.png new file mode 100644 index 0000000..60a3a24 Binary files /dev/null and b/frontend/public/icons/whyChooseUs/compare.png differ diff --git a/frontend/public/ribbon-badge.png b/frontend/public/ribbon-badge.png new file mode 100644 index 0000000..24b18e1 Binary files /dev/null and b/frontend/public/ribbon-badge.png differ diff --git a/frontend/src/app/[category]/[slug]/page.tsx b/frontend/src/app/[category]/[slug]/page.tsx index 11c5820..ada4f27 100644 --- a/frontend/src/app/[category]/[slug]/page.tsx +++ b/frontend/src/app/[category]/[slug]/page.tsx @@ -1,318 +1,99 @@ -import { notFound } from 'next/navigation' -import { Metadata } from 'next' -import Link from 'next/link' -import { ExternalLink, CheckCircle, MapPin, Calendar, Github, Star, MessageSquare } from 'lucide-react' -import { Header } from '@/components/layout/header' -import { Footer } from '@/components/layout/footer' -import { ListingCard } from '@/components/listings/listing-card' -import { getListingBySlug, getAllListings, getRelatedListings } from '@/lib/listings' -import { formatPricing, isIndianCompany, formatDate, getCategoryDisplayName } from '@/lib/utils' +import { notFound } from "next/navigation"; +import { Metadata } from "next"; +import { + getAllProducts, + getProductBySlug, +} from "@/lib/listings"; +import WhyChooseUs from "@/components/pages/productdetails/whyChooseUs"; +import { ProductHeader } from "@/components/pages/productdetails/productHeader"; +import { ProductInfo } from "@/components/pages/productdetails/productinfo"; +import KeyFeatures from "@/components/pages/productdetails/keyFeatures"; +import IntegrationsPage from "@/components/pages/productdetails/integrationsPage"; +import BuyingGuide from "@/components/pages/productdetails/buyingGuide"; +import PricingOverview from "@/components/pages/productdetails/pricingOverview"; interface ProductPageProps { params: Promise<{ - category: string - slug: string - }> + category: string; + slug: string; + }>; } export async function generateStaticParams() { - const listings = await getAllListings() - return listings.map((listing) => ({ - category: listing.category, - slug: listing.slug, - })) + const products = await getAllProducts(); + return products.map((product) => ({ + category: product.categorySlug, + slug: product.slug, + })); } -export async function generateMetadata({ params }: ProductPageProps): Promise { - const { category, slug } = await params - const listing = await getListingBySlug(category, slug) +export async function generateMetadata({ + params, +}: ProductPageProps): Promise { + const { category, slug } = await params; + const product = await getProductBySlug(category, slug); - if (!listing) { + if (!product) { return { - title: 'Product Not Found - AppSutra', - } + title: "Product Not Found - AppSutra", + }; } + // Get first paragraph from description as excerpt + const excerpt = product.description.split('\n\n')[0]; + return { - title: `${listing.name} - ${getCategoryDisplayName(listing.category)} Software | AppSutra`, - description: listing.excerpt || `${listing.name} is a ${listing.category} solution. ${formatPricing(listing.pricing)}. ${listing.trial ? 'Free trial available.' : ''}`, - keywords: [listing.name, ...listing.keywords || [], listing.category, 'software', 'SaaS', 'India'], + title: `${product.name} - ${product.category} Software | AppSutra`, + description: + excerpt || + `${product.name} by ${product.company} is a ${product.category} solution.`, + keywords: [ + + product.name, + ...(product.keywords || []), + product.category, + "software", + "SaaS", + "India", + ], openGraph: { - title: `${listing.name} - ${getCategoryDisplayName(listing.category)} Software`, - description: listing.excerpt || `${listing.name} is a ${listing.category} solution`, - type: 'website', - images: listing.logo ? [{ url: listing.logo }] : [], + title: `${product.name} - ${product.category} Software`, + description: + excerpt || `${product.name} is a ${product.category} solution`, + type: "website", + images: product.icon ? [{ url: product.icon }] : [], }, - } -} - -function parseMarkdownContent(content: string) { - // Simple markdown parsing - in production, you'd use a proper markdown parser - const sections = content.split(/^##\s+/m).filter(Boolean) - const parsed: { [key: string]: string } = {} - - sections.forEach(section => { - const lines = section.trim().split('\n') - const title = lines[0].replace(/^\*\*|\*\*$/g, '').trim() - const body = lines.slice(1).join('\n').trim() - parsed[title.toLowerCase()] = body - }) - - return parsed + }; } export default async function ProductPage({ params }: ProductPageProps) { - const { category, slug } = await params - const listing = await getListingBySlug(category, slug) + const { category, slug } = await params; + const product = await getProductBySlug(category, slug); - if (!listing) { - notFound() + if (!product) { + notFound(); } - const relatedListings = await getRelatedListings(listing, 3) - const categoryName = getCategoryDisplayName(listing.category) - const parsedContent = parseMarkdownContent(listing.content) - return (
-
- - {/* Breadcrumb */} - - - {/* Product Header */} -
-
-
-
-
-
-
-

{listing.name}

- {listing.verified && ( - - )} -
-

{listing.excerpt}

- - {/* Tags */} -
- - {categoryName} - - - {listing.trial && ( - - Free Trial - - )} - - {isIndianCompany(listing.locations) && ( - - ๐Ÿ‡ฎ๐Ÿ‡ณ Indian Company - - )} - - {listing.verified && ( - - Verified - - )} -
-
-
- - {/* Quick Info */} -
-
-

Pricing

-

{formatPricing(listing.pricing)}

-
-
-

Locations

-

{listing.locations.join(', ')}

-
-
-

Last Updated

-

{formatDate(listing.updated_at)}

-
-
+ + +
+ + + + + +
- {/* Action Buttons */} -
- - Visit Website - - - - - - Write Review - - - {listing.listing_owner_github && ( - - - Maintained by {listing.listing_owner_github} - - )} -
-
- - {/* Sidebar */} -
- {/* Use Cases */} - {listing.use_cases && listing.use_cases.length > 0 && ( -
-

Use Cases

-
- {listing.use_cases.map(useCase => ( - - {useCase} - - ))} -
-
- )} - - {/* Integrations */} - {listing.integrations && listing.integrations.length > 0 && ( -
-

Integrations

-
- {listing.integrations.slice(0, 10).map(integration => ( -
- โ€ข {integration} -
- ))} - {listing.integrations.length > 10 && ( -
- +{listing.integrations.length - 10} more -
- )} -
-
- )} - - {/* Contact */} - {listing.contact_email && ( -
-

Contact

- - {listing.contact_email} - -
- )} -
-
-
-
- - {/* Product Content */} -
-
-
-
- {/* Render parsed content sections */} - {parsedContent.about && ( -
-

About

-
- {parsedContent.about} -
-
- )} - - {parsedContent['key features'] && ( -
-

Key Features

-
- {parsedContent['key features']} -
-
- )} - - {parsedContent.pricing && ( -
-

Pricing

-
- {parsedContent.pricing} -
-
- )} - - {parsedContent['for indian businesses'] && ( -
-

For Indian Businesses

-
- {parsedContent['for indian businesses']} -
-
- )} - - {/* Fallback for any remaining content */} - {!parsedContent.about && ( -
- {listing.content} -
- )} -
-
-
-
- - {/* Related Products */} - {relatedListings.length > 0 && ( -
-
-

Related Products

-
- {relatedListings.map((relatedListing) => ( - - ))} -
-
-
- )} - -
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/[category]/page.tsx b/frontend/src/app/[category]/page.tsx index 641ca14..dc54a70 100644 --- a/frontend/src/app/[category]/page.tsx +++ b/frontend/src/app/[category]/page.tsx @@ -1,9 +1,9 @@ import { notFound } from 'next/navigation' import { Metadata } from 'next' -import { Header } from '@/components/layout/header' -import { Footer } from '@/components/layout/footer' -import { ListingCard } from '@/components/listings/listing-card' -import { getListingsByCategory, getCategories } from '@/lib/listings' +// import { Header } from '@/components/layout/header' +// import { Footer } from '@/components/layout/footer' +import { ProductCard } from '@/components/listings/product-card' +import { getProductsByCategory, getCategories } from '@/lib/listings' import { getCategoryDisplayName } from '@/lib/utils' interface CategoryPageProps { @@ -22,9 +22,9 @@ export async function generateStaticParams() { export async function generateMetadata({ params }: CategoryPageProps): Promise { const { category } = await params const categoryName = getCategoryDisplayName(category) - const listings = await getListingsByCategory(category) + const products = await getProductsByCategory(category) - if (listings.length === 0) { + if (products.length === 0) { return { title: 'Category Not Found - AppSutra', } @@ -32,11 +32,11 @@ export async function generateMetadata({ params }: CategoryPageProps): Promise listing.verified) - const unverifiedListings = listings.filter(listing => !listing.verified) - // Stats - const trialCount = listings.filter(listing => listing.trial).length - const indianCount = listings.filter(listing => - listing.locations.some(loc => loc.toLowerCase().includes('india')) + const trialCount = products.filter(product => product.trialPlan).length + const indianCount = products.filter(product => + product.locations.some(loc => loc.toLowerCase().includes('india')) ).length return (
-
{/* Category Hero */}
@@ -73,20 +68,16 @@ export default async function CategoryPage({ params }: CategoryPageProps) { {categoryName} Software

- Discover {listings.length} verified {categoryName.toLowerCase()} solutions + Discover {products.length} verified {categoryName.toLowerCase()} solutions designed for Indian businesses with transparent pricing and community reviews.

{/* Quick Stats */}
- {listings.length} + {products.length} Products
-
- {verifiedListings.length} - Verified -
{trialCount} Free Trials @@ -106,7 +97,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) {

- {listings.length} {categoryName} Solutions + {products.length} {categoryName} Solutions

All solutions are community-verified and regularly updated @@ -126,45 +117,16 @@ export default async function CategoryPage({ params }: CategoryPageProps) {

- {/* Verified Listings */} - {verifiedListings.length > 0 && ( -
-
-

- Verified Solutions -

-

- Hand-picked solutions verified by our expert team -

- -
- {verifiedListings.map((listing) => ( - - ))} -
-
-
- )} - - {/* All Other Listings */} - {unverifiedListings.length > 0 && ( -
-
-

- Community Submitted -

-

- Additional solutions submitted by the community -

- -
- {unverifiedListings.map((listing) => ( - - ))} -
+ {/* Product Listings */} +
+
+
+ {products.map((product) => ( + + ))}
-
- )} +
+
{/* Call to Action */}
@@ -197,7 +159,6 @@ export default async function CategoryPage({ params }: CategoryPageProps) {
-