From 412651e8178cdcbda8d11b78d62d48764ae6e7fe Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 17 Oct 2024 22:35:04 +0200 Subject: [PATCH] feat: improve products & subscriptions page --- apps/api/src/db/payments/payments_products.py | 1 + .../dash/payments/[subpage]/page.tsx | 18 +- .../Payments/PaymentsProductPage.tsx | 237 +++++++++++++----- .../SubComponents/CreateProductForm.tsx | 235 +++++++++-------- .../web/components/Objects/Menus/DashMenu.tsx | 2 +- apps/web/components/ui/badge.tsx | 36 +++ apps/web/package.json | 1 + apps/web/pnpm-lock.yaml | 45 +++- 8 files changed, 380 insertions(+), 195 deletions(-) create mode 100644 apps/web/components/ui/badge.tsx diff --git a/apps/api/src/db/payments/payments_products.py b/apps/api/src/db/payments/payments_products.py index dd4ca872..988594ca 100644 --- a/apps/api/src/db/payments/payments_products.py +++ b/apps/api/src/db/payments/payments_products.py @@ -13,6 +13,7 @@ class PaymentsProductBase(SQLModel): product_type: PaymentProductTypeEnum = PaymentProductTypeEnum.ONE_TIME benefits: str = "" amount: float = 0.0 + currency: str = "USD" class PaymentsProduct(PaymentsProductBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/apps/web/app/orgs/[orgslug]/dash/payments/[subpage]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/payments/[subpage]/page.tsx index 6276e2ca..b17f189b 100644 --- a/apps/web/app/orgs/[orgslug]/dash/payments/[subpage]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/payments/[subpage]/page.tsx @@ -4,7 +4,7 @@ import { motion } from 'framer-motion' import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import Link from 'next/link' import { getUriWithOrg } from '@services/config/config' -import { CreditCard, Settings, Repeat, BookOpen, Users, DollarSign } from 'lucide-react' +import { CreditCard, Settings, Repeat, BookOpen, Users, DollarSign, Gem } from 'lucide-react' import { useLHSession } from '@components/Contexts/LHSessionContext' import { useOrg } from '@components/Contexts/OrgContext' import PaymentsConfigurationPage from '@components/Dashboard/Payments/PaymentsConfigurationPage' @@ -52,7 +52,7 @@ function PaymentsPage({ params }: { params: PaymentsParams }) { } return ( -
+
@@ -75,18 +75,11 @@ function PaymentsPage({ params }: { params: PaymentsParams }) { /> } - label="One-time Products" + icon={} + label="Products & Subscriptions" isActive={selectedSubPage === 'paid-products'} onClick={() => setSelectedSubPage('paid-products')} /> - } - label="Subscriptions" - isActive={selectedSubPage === 'subscriptions'} - onClick={() => setSelectedSubPage('subscriptions')} - /> } @@ -103,12 +96,11 @@ function PaymentsPage({ params }: { params: PaymentsParams }) { animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.1, type: 'spring', stiffness: 80 }} - className="h-full overflow-y-auto" + className="flex-1 overflow-y-auto" > {selectedSubPage === 'general' &&
General
} {selectedSubPage === 'configuration' && } {selectedSubPage === 'paid-products' && } - {selectedSubPage === 'subscriptions' &&
Subscriptions
} {selectedSubPage === 'customers' &&
Customers
}
diff --git a/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx b/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx index e7c88f3a..31c18733 100644 --- a/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx +++ b/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx @@ -1,22 +1,38 @@ 'use client'; -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' +import currencyCodes from 'currency-codes'; import { useOrg } from '@components/Contexts/OrgContext'; import { useLHSession } from '@components/Contexts/LHSessionContext'; import useSWR, { mutate } from 'swr'; import { getProducts, deleteProduct, updateProduct } from '@services/payments/products'; import CreateProductForm from '@components/Dashboard/Payments/SubComponents/CreateProductForm'; -import { Plus, Trash2, Pencil, DollarSign, Info } from 'lucide-react'; +import { Plus, Trash2, Pencil, Info, RefreshCcw, SquareCheck, ChevronDown, ChevronUp } from 'lucide-react'; import Modal from '@components/StyledElements/Modal/Modal'; import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'; import toast from 'react-hot-toast'; -import Link from 'next/link'; -import { getUriWithOrg } from '@services/config/config'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Formik, Form, Field, ErrorMessage } from 'formik'; +import * as Yup from 'yup'; +import { Label } from '@components/ui/label'; +import { Badge } from '@components/ui/badge'; + +const validationSchema = Yup.object().shape({ + name: Yup.string().required('Name is required'), + description: Yup.string().required('Description is required'), + amount: Yup.number().min(0, 'Amount must be positive').required('Amount is required'), + benefits: Yup.string(), + currency: Yup.string().required('Currency is required'), +}); function PaymentsProductPage() { const org = useOrg() as any; const session = useLHSession() as any; const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [editingProductId, setEditingProductId] = useState(null); + const [expandedProducts, setExpandedProducts] = useState<{ [key: string]: boolean }>({}); const { data: products, error } = useSWR( () => org && session ? [`/payments/${org.id}/products`, session.data?.tokens?.access_token] : null, @@ -33,22 +49,20 @@ function PaymentsProductPage() { } } + const toggleProductExpansion = (productId: string) => { + setExpandedProducts(prev => ({ + ...prev, + [productId]: !prev[productId] + })); + }; + if (error) return
Failed to load products
; if (!products) return
Loading...
; return (
-
-

Products

- -
+ {products.data.map((product: any) => ( -
+
{editingProductId === product.id ? ( setEditingProductId(null)} /> ) : ( -
-
-

{product.name}

+
+
+
+ + {product.product_type === 'subscription' ? : } + {product.product_type} + +

{product.name}

+
-

{product.description}

-

${product.amount.toFixed(2)}

-

{product.product_type}

- {product.benefits && ( -
-

Benefits:

-

{product.benefits}

+
+
+

+ {product.description} +

+ {product.benefits && ( +
+

Benefits:

+

+ {product.benefits} +

+
+ )}
- )} +
+
+ +
+
+ Price: + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: product.currency }).format(product.amount)} + +
)}
))}
- {products.data.length === 0 && (

No products available. Create a new product to get started.

)} +
+ +
) } const EditProductForm = ({ product, onSuccess, onCancel }: { product: any, onSuccess: () => void, onCancel: () => void }) => { - const [name, setName] = useState(product.name); - const [description, setDescription] = useState(product.description); - const [amount, setAmount] = useState(product.amount); - const [benefits, setBenefits] = useState(product.benefits || ''); const org = useOrg() as any; const session = useLHSession() as any; + const [currencies, setCurrencies] = useState<{ code: string; name: string }[]>([]); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + useEffect(() => { + const allCurrencies = currencyCodes.data.map(currency => ({ + code: currency.code, + name: `${currency.code} - ${currency.currency}` + })); + setCurrencies(allCurrencies); + }, []); + + const initialValues = { + name: product.name, + description: product.description, + amount: product.amount, + benefits: product.benefits || '', + currency: product.currency || '', + product_type: product.product_type, + }; + + const handleSubmit = async (values: typeof initialValues, { setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void }) => { try { - await updateProduct(org.id, product.id, { name, description, amount, benefits }, session.data?.tokens?.access_token); + await updateProduct(org.id, product.id, values, session.data?.tokens?.access_token); mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]); onSuccess(); toast.success('Product updated successfully'); } catch (error) { toast.error('Failed to update product'); + } finally { + setSubmitting(false); } }; return ( -
- setName(e.target.value)} - className="w-full p-2 border rounded" - placeholder="Product Name" - /> -