@promakeai/cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/registry/auth-core.json +1 -1
- package/dist/registry/blog-core.json +19 -17
- package/dist/registry/blog-list-page.json +2 -2
- package/dist/registry/blog-section.json +1 -1
- package/dist/registry/category-section.json +1 -1
- package/dist/registry/checkout-page.json +1 -1
- package/dist/registry/db.json +6 -6
- package/dist/registry/docs/blog-core.md +11 -6
- package/dist/registry/docs/blog-list-page.md +1 -1
- package/dist/registry/docs/ecommerce-core.md +11 -6
- package/dist/registry/docs/featured-products.md +1 -1
- package/dist/registry/docs/post-detail-page.md +2 -2
- package/dist/registry/docs/product-detail-page.md +2 -2
- package/dist/registry/docs/products-page.md +1 -1
- package/dist/registry/ecommerce-core.json +18 -16
- package/dist/registry/featured-products.json +2 -2
- package/dist/registry/header-ecommerce.json +1 -1
- package/dist/registry/order-confirmation-page.json +1 -1
- package/dist/registry/post-detail-page.json +3 -3
- package/dist/registry/product-detail-page.json +3 -3
- package/dist/registry/products-page.json +2 -2
- package/package.json +1 -1
- package/template/src/lib/api.ts +2 -3
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"path": "checkout-page/checkout-page.tsx",
|
|
25
25
|
"type": "registry:page",
|
|
26
26
|
"target": "$modules$/checkout-page/checkout-page.tsx",
|
|
27
|
-
"content": "import { useState, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { toast } from \"sonner\";\nimport {\n useCart,\n formatPrice,\n type PaymentMethod,\n type OnlinePaymentProvider,\n getFilteredPaymentMethodConfigs,\n getOnlinePaymentProviders,\n ONLINE_PROVIDER_CONFIGS,\n} from \"@/modules/ecommerce-core\";\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\ninterface BankTransferInfo {\n bank_name: string;\n bank_account_name: string;\n iban: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = (constants as any).site?.currency || \"USD\";\n const taxRate = (constants as any).payments?.taxRate || 0;\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\n\n // Calculate shipping and tax\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\n const tax = total * taxRate;\n\n const countries = DEFAULT_COUNTRIES;\n\n // Get available payment methods and providers from config\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\n const availableProviders = getOnlinePaymentProviders();\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\n availablePaymentMethods[0]?.id || \"card\"\n );\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\n availableProviders[0] || \"stripe\"\n );\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n // Bank transfer info state\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\n\n const finalTotal = total + shipping + tax;\n\n // Fetch bank info when transfer is selected\n useEffect(() => {\n if (paymentMethod === \"transfer\") {\n const fetchBankInfo = async () => {\n setIsBankInfoLoading(true);\n setBankInfoError(null);\n try {\n const info = await customerClient.payment.getBankTransferInfo();\n setBankInfo(info);\n } catch (err: any) {\n setBankInfoError(\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\n );\n } finally {\n setIsBankInfoLoading(false);\n }\n };\n fetchBankInfo();\n }\n }, [paymentMethod, t]);\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) {\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n // Determine payment type based on selection\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\n\n if (paymentMethod === \"card\") {\n paymentType = selectedProvider;\n } else if (paymentMethod === \"transfer\") {\n paymentType = \"bank_transfer\";\n } else {\n paymentType = \"cash_on_delivery\";\n }\n\n // Save checkout data to localStorage for success page\n const checkoutData = {\n items: items,\n total: finalTotal,\n customerInfo: formData,\n paymentMethod,\n paymentProvider: paymentType,\n };\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\n\n // Build product data for checkout\n const productData = items.map((item) => {\n const price = getProductPrice(item.product);\n const qty = item.quantity || 1;\n\n return {\n quantity: qty,\n name: item.product.name || \"Product\",\n description: item.product.description || item.product.name || \"Product\",\n amount: Math.round(price * 100), // Convert to cents\n img: item.product.images?.[0] || \"/images/placeholder.png\",\n optionals: {\n productId: item.product.id,\n },\n };\n });\n\n // Tax amount in cents\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\n\n // Create checkout session\n const response = await customerClient.payment.createCheckout({\n currency: currency.toLowerCase(),\n taxAmount: taxAmountInCents,\n paymentType: paymentType,\n productData,\n contactData: {\n firstname: formData.firstName,\n lastname: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n },\n shippingData: {\n address: formData.address,\n country: formData.country,\n city: formData.city,\n zip: formData.postalCode,\n },\n });\n\n // Clear cart and redirect to payment URL\n clearCart();\n window.location.href = response.url;\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Get icon component based on payment method\n const getPaymentIcon = (iconName: string) => {\n switch (iconName) {\n case \"CreditCard\":\n return CreditCard;\n case \"Banknote\":\n return Banknote;\n case \"Truck\":\n return Truck;\n default:\n return CreditCard;\n }\n };\n\n // Get icon color based on payment method\n const getIconColor = (methodId: string) => {\n switch (methodId) {\n case \"card\":\n return \"text-blue-600\";\n case \"transfer\":\n return \"text-primary\";\n case \"cash\":\n return \"text-green-600 dark:text-green-400\";\n default:\n return \"text-primary\";\n }\n };\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {availablePaymentMethods.map((method) => {\n const IconComponent = getPaymentIcon(method.icon);\n const iconColor = getIconColor(method.id);\n\n return (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <IconComponent\n className={`h-5 w-5 ${iconColor}`}\n />\n <div>\n <div className=\"font-medium\">\n {t(method.id, method.label)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(`${method.id}Description`, method.description)}\n </div>\n </div>\n </div>\n </Label>\n </div>\n );\n })}\n </RadioGroup>\n\n {/* Bank Transfer Details */}\n {paymentMethod === \"transfer\" && (\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\n <h4 className=\"font-medium mb-2\">\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\n </h4>\n {isBankInfoLoading ? (\n <div className=\"text-sm space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n ) : bankInfoError ? (\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\n ) : bankInfo ? (\n <div className=\"text-sm space-y-1\">\n <p>\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\n </p>\n <p>\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\n {bankInfo.bank_account_name}\n </p>\n <p>\n <strong>IBAN:</strong> {bankInfo.iban}\n </p>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\n </p>\n )}\n </div>\n )}\n\n {/* Card Payment - Provider Selection */}\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\n <div className=\"mt-4 space-y-4\">\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\n </h4>\n <RadioGroup\n value={selectedProvider}\n onValueChange={(value) =>\n setSelectedProvider(value as OnlinePaymentProvider)\n }\n className=\"space-y-2\"\n >\n {availableProviders.map((provider) => (\n <div\n key={provider}\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\n >\n <RadioGroupItem\n value={provider}\n id={`provider-${provider}`}\n />\n <Label\n htmlFor={`provider-${provider}`}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"font-medium\">\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\n {t(\n \"creditCardRedirectNote\",\n \"You will be redirected to the secure payment page to complete your purchase.\"\n )}\n </p>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <span className=\"text-sm\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </span>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n\nexport default CheckoutPage;\n"
|
|
27
|
+
"content": "import { useState, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { toast } from \"sonner\";\nimport {\n useCart,\n formatPrice,\n type PaymentMethod,\n type OnlinePaymentProvider,\n getFilteredPaymentMethodConfigs,\n getOnlinePaymentProviders,\n ONLINE_PROVIDER_CONFIGS,\n} from \"@/modules/ecommerce-core\";\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\ninterface BankTransferInfo {\n bank_name: string;\n bank_account_name: string;\n iban: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = (constants as any).site?.currency || \"USD\";\n const taxRate = (constants as any).payments?.taxRate || 0;\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\n\n // Calculate shipping and tax\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\n const tax = total * taxRate;\n\n const countries = DEFAULT_COUNTRIES;\n\n // Get available payment methods and providers from config\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\n const availableProviders = getOnlinePaymentProviders();\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\n availablePaymentMethods[0]?.id || \"card\"\n );\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\n availableProviders[0] || \"stripe\"\n );\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n // Bank transfer info state\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\n\n const finalTotal = total + shipping + tax;\n\n // Fetch bank info when transfer is selected\n useEffect(() => {\n if (paymentMethod === \"transfer\") {\n const fetchBankInfo = async () => {\n setIsBankInfoLoading(true);\n setBankInfoError(null);\n try {\n const info = await customerClient.payment.getBankTransferInfo();\n setBankInfo(info);\n } catch (err: any) {\n setBankInfoError(\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\n );\n } finally {\n setIsBankInfoLoading(false);\n }\n };\n fetchBankInfo();\n }\n }, [paymentMethod, t]);\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) {\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n // Determine payment type based on selection\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\n\n if (paymentMethod === \"card\") {\n paymentType = selectedProvider;\n } else if (paymentMethod === \"transfer\") {\n paymentType = \"bank_transfer\";\n } else {\n paymentType = \"cash_on_delivery\";\n }\n\n // Save checkout data to localStorage for success page\n const checkoutData = {\n items: items,\n total: finalTotal,\n customerInfo: formData,\n paymentMethod,\n paymentProvider: paymentType,\n };\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\n\n // Build product data for checkout\n const productData = items.map((item) => {\n const price = getProductPrice(item.product);\n const qty = item.quantity || 1;\n\n return {\n quantity: qty,\n name: item.product.name || \"Product\",\n description: item.product.description || item.product.name || \"Product\",\n amount: Math.round(price * 100), // Convert to cents\n img: item.product.images?.[0] || \"/images/placeholder.png\",\n optionals: {\n productId: item.product.id,\n },\n };\n });\n\n // Tax amount in cents\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\n\n // Create checkout session\n const response = await customerClient.payment.createCheckout({\n currency: currency.toLowerCase(),\n taxAmount: taxAmountInCents,\n paymentType: paymentType,\n productData,\n contactData: {\n firstname: formData.firstName,\n lastname: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n },\n shippingData: {\n address: formData.address,\n country: formData.country,\n city: formData.city,\n zip: formData.postalCode,\n },\n });\n\n // Clear cart and redirect to payment URL or confirmation page\n clearCart();\n if (response.url) {\n window.location.href = response.url;\n } else {\n window.location.href = `/order-confirmation?session_id=${response.sessionId}`;\n }\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Get icon component based on payment method\n const getPaymentIcon = (iconName: string) => {\n switch (iconName) {\n case \"CreditCard\":\n return CreditCard;\n case \"Banknote\":\n return Banknote;\n case \"Truck\":\n return Truck;\n default:\n return CreditCard;\n }\n };\n\n // Get icon color based on payment method\n const getIconColor = (methodId: string) => {\n switch (methodId) {\n case \"card\":\n return \"text-blue-600\";\n case \"transfer\":\n return \"text-primary\";\n case \"cash\":\n return \"text-green-600 dark:text-green-400\";\n default:\n return \"text-primary\";\n }\n };\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {availablePaymentMethods.map((method) => {\n const IconComponent = getPaymentIcon(method.icon);\n const iconColor = getIconColor(method.id);\n\n return (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <IconComponent\n className={`h-5 w-5 ${iconColor}`}\n />\n <div>\n <div className=\"font-medium\">\n {t(method.id, method.label)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(`${method.id}Description`, method.description)}\n </div>\n </div>\n </div>\n </Label>\n </div>\n );\n })}\n </RadioGroup>\n\n {/* Bank Transfer Details */}\n {paymentMethod === \"transfer\" && (\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\n <h4 className=\"font-medium mb-2\">\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\n </h4>\n {isBankInfoLoading ? (\n <div className=\"text-sm space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n ) : bankInfoError ? (\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\n ) : bankInfo ? (\n <div className=\"text-sm space-y-1\">\n <p>\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\n </p>\n <p>\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\n {bankInfo.bank_account_name}\n </p>\n <p>\n <strong>IBAN:</strong> {bankInfo.iban}\n </p>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\n </p>\n )}\n </div>\n )}\n\n {/* Card Payment - Provider Selection */}\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\n <div className=\"mt-4 space-y-4\">\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\n </h4>\n <RadioGroup\n value={selectedProvider}\n onValueChange={(value) =>\n setSelectedProvider(value as OnlinePaymentProvider)\n }\n className=\"space-y-2\"\n >\n {availableProviders.map((provider) => (\n <div\n key={provider}\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\n >\n <RadioGroupItem\n value={provider}\n id={`provider-${provider}`}\n />\n <Label\n htmlFor={`provider-${provider}`}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"font-medium\">\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\n {t(\n \"creditCardRedirectNote\",\n \"You will be redirected to the secure payment page to complete your purchase.\"\n )}\n </p>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <span className=\"text-sm\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </span>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n\nexport default CheckoutPage;\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "checkout-page/lang/en.json",
|
package/dist/registry/db.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"path": "db/index.ts",
|
|
14
14
|
"type": "registry:index",
|
|
15
15
|
"target": "$modules$/db/index.ts",
|
|
16
|
-
"content": "/**\n * DB Module\n * Universal data management with adapter pattern + React Query\n */\n\n// Core\nexport { DataManager } from \"./core/DataManager\";\nexport type {\n QueryOptions,\n OrderBy,\n PaginatedResult,\n // Complex query types\n WhereOperator,\n WhereCondition,\n WhereGroup,\n JoinType,\n JoinClause,\n} from \"./core/types\";\n\n// Adapters\nexport type { IDataAdapter } from \"./adapters/IDataAdapter\";\nexport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\n\n// Config\nexport { getAdapter, createAdapter, resetAdapter } from \"./config\";\nexport type { DataConfig, AdapterType } from \"./config\";\n\n// React exports (re-export from react/index.ts)\nexport * from \"./react\";\n\n// Re-export for convenience\nexport { DBQueryProvider as QueryProvider } from \"./react\";\n\n// Utility exports (parsers for client-side data transformation)\nexport {\n parseCommaSeparatedString,\n parseJSONStringToArray,\n parseStringToArray,\n parseJSONString,\n parseSQLiteBoolean,\n parseNumberSafe,\n} from \"./utils/parsers\";\n"
|
|
16
|
+
"content": "/**\r\n * DB Module\r\n * Universal data management with adapter pattern + React Query\r\n */\r\n\r\n// Core\r\nexport { DataManager } from \"./core/DataManager\";\r\nexport type {\r\n QueryOptions,\r\n OrderBy,\r\n PaginatedResult,\r\n // Complex query types\r\n WhereOperator,\r\n WhereCondition,\r\n WhereGroup,\r\n JoinType,\r\n JoinClause,\r\n} from \"./core/types\";\r\n\r\n// Adapters\r\nexport type { IDataAdapter } from \"./adapters/IDataAdapter\";\r\nexport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\r\n\r\n// Config\r\nexport { getAdapter, createAdapter, resetAdapter } from \"./config\";\r\nexport type { DataConfig, AdapterType } from \"./config\";\r\n\r\n// React exports (re-export from react/index.ts)\r\nexport * from \"./react\";\r\n\r\n// Re-export for convenience\r\nexport { DBQueryProvider as QueryProvider } from \"./react\";\r\n\r\n// Utility exports (parsers for client-side data transformation)\r\nexport {\r\n parseCommaSeparatedString,\r\n parseJSONStringToArray,\r\n parseStringToArray,\r\n parseJSONString,\r\n parseSQLiteBoolean,\r\n parseNumberSafe,\r\n} from \"./utils/parsers\";\r\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "db/config.ts",
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"path": "db/core/DataManager.ts",
|
|
26
26
|
"type": "registry:lib",
|
|
27
27
|
"target": "$modules$/db/core/DataManager.ts",
|
|
28
|
-
"content": "import type { IDataAdapter } from \"../adapters/IDataAdapter\";\nimport type { QueryOptions, PaginatedResult } from \"./types\";\n\n/**\n * DataManager - Simple proxy to adapter\n */\nexport class DataManager {\n private static instance: DataManager;\n private adapter: IDataAdapter;\n\n private constructor(adapter: IDataAdapter) {\n this.adapter = adapter;\n }\n\n static getInstance(adapter: IDataAdapter): DataManager {\n if (!DataManager.instance) {\n DataManager.instance = new DataManager(adapter);\n }\n return DataManager.instance;\n }\n\n // Simple pass-through methods - no cache logic\n async query<T = any>(table: string, options?: QueryOptions): Promise<T[]> {\n return this.adapter.findMany<T>(table, options);\n }\n\n async queryOne<T = any>(\n table: string,\n options?: QueryOptions,\n ): Promise<T | null> {\n const results = await this.query<T>(table, { ...options, limit: 1 });\n return results[0] || null;\n }\n\n async queryById<T = any>(\n table: string,\n id: number | string,\n ): Promise<T | null> {\n return this.adapter.findById<T>(table, id);\n }\n\n async count(table: string, options?: QueryOptions): Promise<number> {\n return this.adapter.count(table, options);\n }\n\n async paginate<T = any>(\n table: string,\n page: number = 1,\n limit: number = 10,\n options?: QueryOptions,\n ): Promise<PaginatedResult<T>> {\n const offset = (page - 1) * limit;\n\n const [data, total] = await Promise.all([\n this.query<T>(table, { ...options, limit, offset }),\n this.count(table, options),\n ]);\n\n return {\n data,\n page,\n limit,\n total,\n totalPages: Math.ceil(total / limit),\n hasMore: page * limit < total,\n };\n }\n\n // Mutations - no cache invalidation here (React Query handles it)\n async create<T = any>(table: string, data: Partial<T>): Promise<T> {\n return this.adapter.create<T>(table, data);\n }\n\n async update<T = any>(\n table: string,\n id: number | string,\n data: Partial<T>,\n ): Promise<T> {\n return this.adapter.update<T>(table, id, data);\n }\n\n async delete(table: string, id: number | string): Promise<boolean> {\n return this.adapter.delete(table, id);\n }\n\n // Direct adapter access for advanced use cases\n getAdapter(): IDataAdapter {\n return this.adapter;\n }\n\n // ============================================\n // RAW SQL QUERIES\n // ============================================\n\n /**\n * Execute raw SQL query - returns multiple rows\n * Use for complex queries that can't be expressed with QueryOptions\n *\n * @example\n * const posts = await dm.raw<Post>(`\n * SELECT DISTINCT p.*, c.name as category_name\n * FROM posts p\n * JOIN post_categories pc ON p.id = pc.post_id\n * JOIN blog_categories c ON pc.category_id = c.id\n * WHERE c.slug = ? AND p.published = 1\n * `, [categorySlug]);\n */\n async raw<T = any>(sql: string, params?: any[]): Promise<T[]> {\n return this.adapter.raw<T>(sql, params);\n }\n\n /**\n * Execute raw SQL query - returns single row or null\n * Use for aggregations, single record lookups\n *\n * @example\n * const priceRange = await dm.rawOne<{min: number, max: number}>(`\n * SELECT MIN(price) as min, MAX(price) as max\n * FROM products WHERE published = 1\n * `);\n */\n async rawOne<T = any>(sql: string, params?: any[]): Promise<T | null> {\n return this.adapter.rawOne<T>(sql, params);\n }\n}\n"
|
|
28
|
+
"content": "import type { IDataAdapter } from \"../adapters/IDataAdapter\";\r\nimport type { QueryOptions, PaginatedResult } from \"./types\";\r\n\r\n/**\r\n * DataManager - Simple proxy to adapter\r\n */\r\nexport class DataManager {\r\n private static instance: DataManager;\r\n private adapter: IDataAdapter;\r\n\r\n private constructor(adapter: IDataAdapter) {\r\n this.adapter = adapter;\r\n }\r\n\r\n static getInstance(adapter: IDataAdapter): DataManager {\r\n if (!DataManager.instance) {\r\n DataManager.instance = new DataManager(adapter);\r\n }\r\n return DataManager.instance;\r\n }\r\n\r\n // Simple pass-through methods - no cache logic\r\n async query<T = any>(table: string, options?: QueryOptions): Promise<T[]> {\r\n return this.adapter.findMany<T>(table, options);\r\n }\r\n\r\n async queryOne<T = any>(\r\n table: string,\r\n options?: QueryOptions,\r\n ): Promise<T | null> {\r\n const results = await this.query<T>(table, { ...options, limit: 1 });\r\n return results[0] || null;\r\n }\r\n\r\n async queryById<T = any>(\r\n table: string,\r\n id: number | string,\r\n ): Promise<T | null> {\r\n return this.adapter.findById<T>(table, id);\r\n }\r\n\r\n async count(table: string, options?: QueryOptions): Promise<number> {\r\n return this.adapter.count(table, options);\r\n }\r\n\r\n async paginate<T = any>(\r\n table: string,\r\n page: number = 1,\r\n limit: number = 10,\r\n options?: QueryOptions,\r\n ): Promise<PaginatedResult<T>> {\r\n const offset = (page - 1) * limit;\r\n\r\n const [data, total] = await Promise.all([\r\n this.query<T>(table, { ...options, limit, offset }),\r\n this.count(table, options),\r\n ]);\r\n\r\n return {\r\n data,\r\n page,\r\n limit,\r\n total,\r\n totalPages: Math.ceil(total / limit),\r\n hasMore: page * limit < total,\r\n };\r\n }\r\n\r\n // Mutations - no cache invalidation here (React Query handles it)\r\n async create<T = any>(table: string, data: Partial<T>): Promise<T> {\r\n return this.adapter.create<T>(table, data);\r\n }\r\n\r\n async update<T = any>(\r\n table: string,\r\n id: number | string,\r\n data: Partial<T>,\r\n ): Promise<T> {\r\n return this.adapter.update<T>(table, id, data);\r\n }\r\n\r\n async delete(table: string, id: number | string): Promise<boolean> {\r\n return this.adapter.delete(table, id);\r\n }\r\n\r\n // Direct adapter access for advanced use cases\r\n getAdapter(): IDataAdapter {\r\n return this.adapter;\r\n }\r\n\r\n // ============================================\r\n // RAW SQL QUERIES\r\n // ============================================\r\n\r\n /**\r\n * Execute raw SQL query - returns multiple rows\r\n * Use for complex queries that can't be expressed with QueryOptions\r\n *\r\n * @example\r\n * const posts = await dm.raw<Post>(`\r\n * SELECT DISTINCT p.*, c.name as category_name\r\n * FROM posts p\r\n * JOIN post_categories pc ON p.id = pc.post_id\r\n * JOIN blog_categories c ON pc.category_id = c.id\r\n * WHERE c.slug = ? AND p.published = 1\r\n * `, [categorySlug]);\r\n */\r\n async raw<T = any>(sql: string, params?: any[]): Promise<T[]> {\r\n return this.adapter.raw<T>(sql, params);\r\n }\r\n\r\n /**\r\n * Execute raw SQL query - returns single row or null\r\n * Use for aggregations, single record lookups\r\n *\r\n * @example\r\n * const priceRange = await dm.rawOne<{min: number, max: number}>(`\r\n * SELECT MIN(price) as min, MAX(price) as max\r\n * FROM products WHERE published = 1\r\n * `);\r\n */\r\n async rawOne<T = any>(sql: string, params?: any[]): Promise<T | null> {\r\n return this.adapter.rawOne<T>(sql, params);\r\n }\r\n}\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "db/core/types.ts",
|
|
32
32
|
"type": "registry:type",
|
|
33
33
|
"target": "$modules$/db/core/types.ts",
|
|
34
|
-
"content": "/**\n * Core type definitions for data-access module\n */\n\n// ============================================\n// WHERE CONDITIONS\n// ============================================\n\n/** WHERE condition operators */\nexport type WhereOperator =\n | \"=\"\n | \"!=\"\n | \"<>\"\n | \">\"\n | \"<\"\n | \">=\"\n | \"<=\"\n | \"LIKE\"\n | \"NOT LIKE\"\n | \"IN\"\n | \"NOT IN\"\n | \"BETWEEN\"\n | \"NOT BETWEEN\"\n | \"IS NULL\"\n | \"IS NOT NULL\";\n\n/** Single WHERE condition */\nexport interface WhereCondition {\n field: string;\n operator: WhereOperator;\n value: any;\n}\n\n/** WHERE groups (AND/OR) - recursive structure */\nexport interface WhereGroup {\n type: \"AND\" | \"OR\";\n conditions: (WhereCondition | WhereGroup)[];\n}\n\n// ============================================\n// JOIN DEFINITIONS\n// ============================================\n\n/** JOIN types */\nexport type JoinType = \"INNER\" | \"LEFT\" | \"RIGHT\" | \"CROSS\";\n\n/** JOIN definition */\nexport interface JoinClause {\n type: JoinType;\n table: string;\n alias?: string;\n on: {\n leftField: string;\n rightField: string;\n };\n}\n\n// ============================================\n// QUERY OPTIONS\n// ============================================\n\n/** Order definition */\nexport interface OrderBy {\n field: string;\n direction: \"ASC\" | \"DESC\";\n}\n\n/** Extended Query Options */\nexport interface QueryOptions {\n // Existing (backwards compatible)\n where?: Record<string, any>;\n limit?: number;\n offset?: number;\n orderBy?: OrderBy[];\n include?: string[];\n\n // New features\n select?: string[]; // SELECT fields: ['p.*', 'c.name as category_name']\n distinct?: boolean; // DISTINCT usage\n joins?: JoinClause[]; // JOIN definitions\n whereAdvanced?: WhereGroup; // Complex WHERE conditions (AND/OR groups)\n groupBy?: string[]; // GROUP BY fields\n having?: WhereGroup; // HAVING conditions\n}\n\n// ============================================\n// RESULT TYPES\n// ============================================\n\n/** Paginated result */\nexport interface PaginatedResult<T> {\n data: T[];\n page: number;\n limit: number;\n total: number;\n totalPages: number;\n hasMore: boolean;\n}\n\n/** Query key (for cache) */\nexport type QueryKey = (string | number | object)[];\n"
|
|
34
|
+
"content": "/**\r\n * Core type definitions for data-access module\r\n */\r\n\r\n// ============================================\r\n// WHERE CONDITIONS\r\n// ============================================\r\n\r\n/** WHERE condition operators */\r\nexport type WhereOperator =\r\n | \"=\"\r\n | \"!=\"\r\n | \"<>\"\r\n | \">\"\r\n | \"<\"\r\n | \">=\"\r\n | \"<=\"\r\n | \"LIKE\"\r\n | \"NOT LIKE\"\r\n | \"IN\"\r\n | \"NOT IN\"\r\n | \"BETWEEN\"\r\n | \"NOT BETWEEN\"\r\n | \"IS NULL\"\r\n | \"IS NOT NULL\";\r\n\r\n/** Single WHERE condition */\r\nexport interface WhereCondition {\r\n field: string;\r\n operator: WhereOperator;\r\n value: any;\r\n}\r\n\r\n/** WHERE groups (AND/OR) - recursive structure */\r\nexport interface WhereGroup {\r\n type: \"AND\" | \"OR\";\r\n conditions: (WhereCondition | WhereGroup)[];\r\n}\r\n\r\n// ============================================\r\n// JOIN DEFINITIONS\r\n// ============================================\r\n\r\n/** JOIN types */\r\nexport type JoinType = \"INNER\" | \"LEFT\" | \"RIGHT\" | \"CROSS\";\r\n\r\n/** JOIN definition */\r\nexport interface JoinClause {\r\n type: JoinType;\r\n table: string;\r\n alias?: string;\r\n on: {\r\n leftField: string;\r\n rightField: string;\r\n };\r\n}\r\n\r\n// ============================================\r\n// QUERY OPTIONS\r\n// ============================================\r\n\r\n/** Order definition */\r\nexport interface OrderBy {\r\n field: string;\r\n direction: \"ASC\" | \"DESC\";\r\n}\r\n\r\n/** Extended Query Options */\r\nexport interface QueryOptions {\r\n // Existing (backwards compatible)\r\n where?: Record<string, any>;\r\n limit?: number;\r\n offset?: number;\r\n orderBy?: OrderBy[];\r\n include?: string[];\r\n\r\n // New features\r\n select?: string[]; // SELECT fields: ['p.*', 'c.name as category_name']\r\n distinct?: boolean; // DISTINCT usage\r\n joins?: JoinClause[]; // JOIN definitions\r\n whereAdvanced?: WhereGroup; // Complex WHERE conditions (AND/OR groups)\r\n groupBy?: string[]; // GROUP BY fields\r\n having?: WhereGroup; // HAVING conditions\r\n}\r\n\r\n// ============================================\r\n// RESULT TYPES\r\n// ============================================\r\n\r\n/** Paginated result */\r\nexport interface PaginatedResult<T> {\r\n data: T[];\r\n page: number;\r\n limit: number;\r\n total: number;\r\n totalPages: number;\r\n hasMore: boolean;\r\n}\r\n\r\n/** Query key (for cache) */\r\nexport type QueryKey = (string | number | object)[];\r\n"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "db/adapters/IDataAdapter.ts",
|
|
38
38
|
"type": "registry:type",
|
|
39
39
|
"target": "$modules$/db/adapters/IDataAdapter.ts",
|
|
40
|
-
"content": "import type { QueryOptions } from \"../core/types\";\n\n/**\n * Data Adapter Interface\n * Implement this interface to create custom data sources\n */\nexport interface IDataAdapter {\n // Connection\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n\n // CRUD operations\n findMany<T>(table: string, options?: QueryOptions): Promise<T[]>;\n findOne<T>(table: string, options: QueryOptions): Promise<T | null>;\n findById<T>(table: string, id: number | string): Promise<T | null>;\n create<T>(table: string, data: Partial<T>): Promise<T>;\n update<T>(table: string, id: number | string, data: Partial<T>): Promise<T>;\n delete(table: string, id: number | string): Promise<boolean>;\n\n // Utilities\n count(table: string, options?: QueryOptions): Promise<number>;\n\n // Raw SQL queries - for complex queries that can't be expressed with QueryOptions\n raw<T>(sql: string, params?: any[]): Promise<T[]>;\n rawOne<T>(sql: string, params?: any[]): Promise<T | null>;\n}\n"
|
|
40
|
+
"content": "import type { QueryOptions } from \"../core/types\";\r\n\r\n/**\r\n * Data Adapter Interface\r\n * Implement this interface to create custom data sources\r\n */\r\nexport interface IDataAdapter {\r\n // Connection\r\n connect(): Promise<void>;\r\n disconnect(): Promise<void>;\r\n\r\n // CRUD operations\r\n findMany<T>(table: string, options?: QueryOptions): Promise<T[]>;\r\n findOne<T>(table: string, options: QueryOptions): Promise<T | null>;\r\n findById<T>(table: string, id: number | string): Promise<T | null>;\r\n create<T>(table: string, data: Partial<T>): Promise<T>;\r\n update<T>(table: string, id: number | string, data: Partial<T>): Promise<T>;\r\n delete(table: string, id: number | string): Promise<boolean>;\r\n\r\n // Utilities\r\n count(table: string, options?: QueryOptions): Promise<number>;\r\n\r\n // Raw SQL queries - for complex queries that can't be expressed with QueryOptions\r\n raw<T>(sql: string, params?: any[]): Promise<T[]>;\r\n rawOne<T>(sql: string, params?: any[]): Promise<T | null>;\r\n}\r\n"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"path": "db/adapters/index.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"path": "db/adapters/SqliteAdapter.ts",
|
|
50
50
|
"type": "registry:lib",
|
|
51
51
|
"target": "$modules$/db/adapters/SqliteAdapter.ts",
|
|
52
|
-
"content": "import initSqlJs from \"sql.js\";\nimport type { Database } from \"sql.js\";\nimport type { IDataAdapter } from \"./IDataAdapter\";\nimport type {\n QueryOptions,\n WhereCondition,\n WhereGroup,\n JoinClause,\n} from \"../core/types\";\n\n/**\n * SQLite Adapter\n * Loads database from file using sql.js\n * Supports complex queries: JOIN, WHERE groups, LIKE, IN, BETWEEN\n */\nexport class SqliteAdapter implements IDataAdapter {\n private db: Database | null = null;\n private dbPath: string;\n\n constructor(dbPath: string = \"/data/database.db\") {\n this.dbPath = dbPath;\n }\n\n // ============================================\n // CONNECTION\n // ============================================\n\n async connect(): Promise<void> {\n if (this.db) return;\n\n try {\n const SQL = await initSqlJs({\n locateFile: (file: string) => `https://sql.js.org/dist/${file}`,\n });\n\n const response = await fetch(this.dbPath);\n if (!response.ok) {\n throw new Error(`Database file not found: ${this.dbPath}`);\n }\n\n const buffer = await response.arrayBuffer();\n this.db = new SQL.Database(new Uint8Array(buffer));\n console.log(\"SQLite adapter connected\");\n } catch (error) {\n console.error(\"SQLite adapter connection failed:\", error);\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n // ============================================\n // CRUD OPERATIONS\n // ============================================\n\n async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {\n await this.connect();\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const { sql, params } = this.buildSelectQuery(table, options);\n return this.executeQuery<T>(sql, params);\n } catch (error) {\n console.error(`Error querying table ${table}:`, error);\n return [];\n }\n }\n\n async findOne<T>(table: string, options: QueryOptions): Promise<T | null> {\n const results = await this.findMany<T>(table, { ...options, limit: 1 });\n return results[0] || null;\n }\n\n async findById<T>(table: string, id: number | string): Promise<T | null> {\n return this.findOne<T>(table, { where: { id } });\n }\n\n async create<T>(table: string, data: Partial<T>): Promise<T> {\n await this.connect();\n\n // Otomatik timestamp - sadece yoksa ekle\n const dataWithTimestamps: any = { ...data };\n\n if (!dataWithTimestamps.created_at) {\n dataWithTimestamps.created_at = new Date().toISOString();\n }\n\n if (!dataWithTimestamps.updated_at) {\n dataWithTimestamps.updated_at = new Date().toISOString();\n }\n\n const keys = Object.keys(dataWithTimestamps);\n const values = Object.values(dataWithTimestamps) as any[];\n const placeholders = keys.map(() => \"?\").join(\", \");\n\n const sql = `INSERT INTO ${table} (${keys.join(\", \")}) VALUES (${placeholders})`;\n this.db!.run(sql, values);\n\n const lastIdResult = this.db!.exec(\"SELECT last_insert_rowid() as id\");\n const lastId = lastIdResult[0]?.values[0]?.[0] as number;\n\n return this.findById<T>(table, lastId) as Promise<T>;\n }\n\n async update<T>(\n table: string,\n id: number | string,\n data: Partial<T>,\n ): Promise<T> {\n await this.connect();\n\n // Otomatik updated_at - her zaman güncelle\n const dataWithTimestamp = {\n ...data,\n updated_at: new Date().toISOString(),\n };\n\n const keys = Object.keys(dataWithTimestamp);\n const values = Object.values(dataWithTimestamp) as any[];\n const setClause = keys.map((key) => `${key} = ?`).join(\", \");\n\n const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;\n this.db!.run(sql, [...values, id]);\n\n return this.findById<T>(table, id) as Promise<T>;\n }\n\n async delete(table: string, id: number | string): Promise<boolean> {\n await this.connect();\n const sql = `DELETE FROM ${table} WHERE id = ?`;\n this.db!.run(sql, [id]);\n return true;\n }\n\n async count(table: string, options?: QueryOptions): Promise<number> {\n await this.connect();\n const { sql, params } = this.buildCountQuery(table, options);\n const results = await this.executeQuery<{ count: number }>(sql, params);\n return results[0]?.count || 0;\n }\n\n // ============================================\n // RAW SQL QUERIES\n // ============================================\n\n async raw<T>(sql: string, params?: any[]): Promise<T[]> {\n await this.connect();\n return this.executeQuery<T>(sql, params);\n }\n\n async rawOne<T>(sql: string, params?: any[]): Promise<T | null> {\n const results = await this.raw<T>(sql, params);\n return results[0] || null;\n }\n\n // ============================================\n // PRIVATE: QUERY EXECUTION\n // ============================================\n\n private async executeQuery<T>(sql: string, params?: any[]): Promise<T[]> {\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const stmt = this.db.prepare(sql);\n if (params && params.length > 0) stmt.bind(params);\n\n const results: T[] = [];\n while (stmt.step()) {\n results.push(stmt.getAsObject() as T);\n }\n stmt.free();\n return results;\n } catch (error) {\n console.error(`Query error: ${sql}`, error);\n return [];\n }\n }\n\n // ============================================\n // PRIVATE: QUERY BUILDERS\n // ============================================\n\n private buildSelectQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n\n // SELECT clause\n const selectFields = options?.select?.join(\", \") || \"*\";\n const distinct = options?.distinct ? \"DISTINCT \" : \"\";\n let sql = `SELECT ${distinct}${selectFields} FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n // GROUP BY clause\n if (options?.groupBy && options.groupBy.length > 0) {\n sql += ` GROUP BY ${options.groupBy.join(\", \")}`;\n }\n\n // HAVING clause\n if (options?.having) {\n const havingClause = this.buildWhereGroupClause(options.having, params);\n if (havingClause) {\n sql += ` HAVING ${havingClause}`;\n }\n }\n\n // ORDER BY clause\n if (options?.orderBy && options.orderBy.length > 0) {\n const orderClauses = options.orderBy.map(\n (o) => `${o.field} ${o.direction}`,\n );\n sql += ` ORDER BY ${orderClauses.join(\", \")}`;\n }\n\n // LIMIT & OFFSET\n if (options?.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n if (options?.offset) {\n sql += ` OFFSET ?`;\n params.push(options.offset);\n }\n\n return { sql, params };\n }\n\n private buildCountQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n let sql = `SELECT COUNT(*) as count FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n return { sql, params };\n }\n\n private buildJoinClause(joins: JoinClause[]): string {\n return joins\n .map((join) => {\n const alias = join.alias ? ` ${join.alias}` : \"\";\n const leftField = join.on.leftField;\n const rightField = join.on.rightField;\n return ` ${join.type} JOIN ${join.table}${alias} ON ${leftField} = ${rightField}`;\n })\n .join(\"\");\n }\n\n private buildWhereClause(\n options: QueryOptions | undefined,\n params: any[],\n ): string {\n const clauses: string[] = [];\n\n // Simple where (backwards compatible)\n if (options?.where) {\n const simpleConditions = Object.entries(options.where).map(\n ([key, value]) => {\n params.push(value);\n return `${key} = ?`;\n },\n );\n if (simpleConditions.length > 0) {\n clauses.push(simpleConditions.join(\" AND \"));\n }\n }\n\n // Advanced where (new feature)\n if (options?.whereAdvanced) {\n const advancedClause = this.buildWhereGroupClause(\n options.whereAdvanced,\n params,\n );\n if (advancedClause) {\n clauses.push(advancedClause);\n }\n }\n\n return clauses.length > 0 ? clauses.join(\" AND \") : \"\";\n }\n\n private buildWhereGroupClause(group: WhereGroup, params: any[]): string {\n const conditions = group.conditions\n .map((condition) => {\n // Recursive: WhereGroup\n if (\"type\" in condition && \"conditions\" in condition) {\n return `(${this.buildWhereGroupClause(condition as WhereGroup, params)})`;\n }\n // WhereCondition\n return this.buildConditionClause(condition as WhereCondition, params);\n })\n .filter(Boolean);\n\n return conditions.join(` ${group.type} `);\n }\n\n private buildConditionClause(\n condition: WhereCondition,\n params: any[],\n ): string {\n const { field, operator, value } = condition;\n\n switch (operator) {\n case \"IS NULL\":\n return `${field} IS NULL`;\n\n case \"IS NOT NULL\":\n return `${field} IS NOT NULL`;\n\n case \"IN\":\n case \"NOT IN\":\n if (Array.isArray(value)) {\n const placeholders = value.map(() => \"?\").join(\", \");\n params.push(...value);\n return `${field} ${operator} (${placeholders})`;\n }\n return \"\";\n\n case \"BETWEEN\":\n case \"NOT BETWEEN\":\n if (Array.isArray(value) && value.length === 2) {\n params.push(value[0], value[1]);\n return `${field} ${operator} ? AND ?`;\n }\n return \"\";\n\n default:\n // =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE\n params.push(value);\n return `${field} ${operator} ?`;\n }\n }\n}\n"
|
|
52
|
+
"content": "import initSqlJs from \"sql.js\";\r\nimport type { Database } from \"sql.js\";\r\nimport type { IDataAdapter } from \"./IDataAdapter\";\r\nimport type {\r\n QueryOptions,\r\n WhereCondition,\r\n WhereGroup,\r\n JoinClause,\r\n} from \"../core/types\";\r\n\r\n/**\r\n * SQLite Adapter\r\n * Loads database from file using sql.js\r\n * Supports complex queries: JOIN, WHERE groups, LIKE, IN, BETWEEN\r\n */\r\nexport class SqliteAdapter implements IDataAdapter {\r\n private db: Database | null = null;\r\n private dbPath: string;\r\n\r\n constructor(dbPath: string = \"/data/database.db\") {\r\n this.dbPath = dbPath;\r\n }\r\n\r\n // ============================================\r\n // CONNECTION\r\n // ============================================\r\n\r\n async connect(): Promise<void> {\r\n if (this.db) return;\r\n\r\n try {\r\n const SQL = await initSqlJs({\r\n locateFile: (file: string) => `https://sql.js.org/dist/${file}`,\r\n });\r\n\r\n const response = await fetch(this.dbPath);\r\n if (!response.ok) {\r\n throw new Error(`Database file not found: ${this.dbPath}`);\r\n }\r\n\r\n const buffer = await response.arrayBuffer();\r\n this.db = new SQL.Database(new Uint8Array(buffer));\r\n console.log(\"SQLite adapter connected\");\r\n } catch (error) {\r\n console.error(\"SQLite adapter connection failed:\", error);\r\n throw error;\r\n }\r\n }\r\n\r\n async disconnect(): Promise<void> {\r\n if (this.db) {\r\n this.db.close();\r\n this.db = null;\r\n }\r\n }\r\n\r\n // ============================================\r\n // CRUD OPERATIONS\r\n // ============================================\r\n\r\n async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {\r\n await this.connect();\r\n if (!this.db) {\r\n console.warn(\"Database not connected\");\r\n return [];\r\n }\r\n try {\r\n const { sql, params } = this.buildSelectQuery(table, options);\r\n return this.executeQuery<T>(sql, params);\r\n } catch (error) {\r\n console.error(`Error querying table ${table}:`, error);\r\n return [];\r\n }\r\n }\r\n\r\n async findOne<T>(table: string, options: QueryOptions): Promise<T | null> {\r\n const results = await this.findMany<T>(table, { ...options, limit: 1 });\r\n return results[0] || null;\r\n }\r\n\r\n async findById<T>(table: string, id: number | string): Promise<T | null> {\r\n return this.findOne<T>(table, { where: { id } });\r\n }\r\n\r\n async create<T>(table: string, data: Partial<T>): Promise<T> {\r\n await this.connect();\r\n\r\n // Otomatik timestamp - sadece yoksa ekle\r\n const dataWithTimestamps: any = { ...data };\r\n\r\n if (!dataWithTimestamps.created_at) {\r\n dataWithTimestamps.created_at = new Date().toISOString();\r\n }\r\n\r\n if (!dataWithTimestamps.updated_at) {\r\n dataWithTimestamps.updated_at = new Date().toISOString();\r\n }\r\n\r\n const keys = Object.keys(dataWithTimestamps);\r\n const values = Object.values(dataWithTimestamps) as any[];\r\n const placeholders = keys.map(() => \"?\").join(\", \");\r\n\r\n const sql = `INSERT INTO ${table} (${keys.join(\", \")}) VALUES (${placeholders})`;\r\n this.db!.run(sql, values);\r\n\r\n const lastIdResult = this.db!.exec(\"SELECT last_insert_rowid() as id\");\r\n const lastId = lastIdResult[0]?.values[0]?.[0] as number;\r\n\r\n return this.findById<T>(table, lastId) as Promise<T>;\r\n }\r\n\r\n async update<T>(\r\n table: string,\r\n id: number | string,\r\n data: Partial<T>,\r\n ): Promise<T> {\r\n await this.connect();\r\n\r\n // Otomatik updated_at - her zaman güncelle\r\n const dataWithTimestamp = {\r\n ...data,\r\n updated_at: new Date().toISOString(),\r\n };\r\n\r\n const keys = Object.keys(dataWithTimestamp);\r\n const values = Object.values(dataWithTimestamp) as any[];\r\n const setClause = keys.map((key) => `${key} = ?`).join(\", \");\r\n\r\n const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;\r\n this.db!.run(sql, [...values, id]);\r\n\r\n return this.findById<T>(table, id) as Promise<T>;\r\n }\r\n\r\n async delete(table: string, id: number | string): Promise<boolean> {\r\n await this.connect();\r\n const sql = `DELETE FROM ${table} WHERE id = ?`;\r\n this.db!.run(sql, [id]);\r\n return true;\r\n }\r\n\r\n async count(table: string, options?: QueryOptions): Promise<number> {\r\n await this.connect();\r\n const { sql, params } = this.buildCountQuery(table, options);\r\n const results = await this.executeQuery<{ count: number }>(sql, params);\r\n return results[0]?.count || 0;\r\n }\r\n\r\n // ============================================\r\n // RAW SQL QUERIES\r\n // ============================================\r\n\r\n async raw<T>(sql: string, params?: any[]): Promise<T[]> {\r\n await this.connect();\r\n return this.executeQuery<T>(sql, params);\r\n }\r\n\r\n async rawOne<T>(sql: string, params?: any[]): Promise<T | null> {\r\n const results = await this.raw<T>(sql, params);\r\n return results[0] || null;\r\n }\r\n\r\n // ============================================\r\n // PRIVATE: QUERY EXECUTION\r\n // ============================================\r\n\r\n private async executeQuery<T>(sql: string, params?: any[]): Promise<T[]> {\r\n if (!this.db) {\r\n console.warn(\"Database not connected\");\r\n return [];\r\n }\r\n try {\r\n const stmt = this.db.prepare(sql);\r\n if (params && params.length > 0) stmt.bind(params);\r\n\r\n const results: T[] = [];\r\n while (stmt.step()) {\r\n results.push(stmt.getAsObject() as T);\r\n }\r\n stmt.free();\r\n return results;\r\n } catch (error) {\r\n console.error(`Query error: ${sql}`, error);\r\n return [];\r\n }\r\n }\r\n\r\n // ============================================\r\n // PRIVATE: QUERY BUILDERS\r\n // ============================================\r\n\r\n private buildSelectQuery(\r\n table: string,\r\n options?: QueryOptions,\r\n ): { sql: string; params: any[] } {\r\n const params: any[] = [];\r\n\r\n // SELECT clause\r\n const selectFields = options?.select?.join(\", \") || \"*\";\r\n const distinct = options?.distinct ? \"DISTINCT \" : \"\";\r\n let sql = `SELECT ${distinct}${selectFields} FROM ${table}`;\r\n\r\n // JOIN clauses\r\n if (options?.joins && options.joins.length > 0) {\r\n sql += this.buildJoinClause(options.joins);\r\n }\r\n\r\n // WHERE clause\r\n const whereClause = this.buildWhereClause(options, params);\r\n if (whereClause) {\r\n sql += ` WHERE ${whereClause}`;\r\n }\r\n\r\n // GROUP BY clause\r\n if (options?.groupBy && options.groupBy.length > 0) {\r\n sql += ` GROUP BY ${options.groupBy.join(\", \")}`;\r\n }\r\n\r\n // HAVING clause\r\n if (options?.having) {\r\n const havingClause = this.buildWhereGroupClause(options.having, params);\r\n if (havingClause) {\r\n sql += ` HAVING ${havingClause}`;\r\n }\r\n }\r\n\r\n // ORDER BY clause\r\n if (options?.orderBy && options.orderBy.length > 0) {\r\n const orderClauses = options.orderBy.map(\r\n (o) => `${o.field} ${o.direction}`,\r\n );\r\n sql += ` ORDER BY ${orderClauses.join(\", \")}`;\r\n }\r\n\r\n // LIMIT & OFFSET\r\n if (options?.limit) {\r\n sql += ` LIMIT ?`;\r\n params.push(options.limit);\r\n }\r\n\r\n if (options?.offset) {\r\n sql += ` OFFSET ?`;\r\n params.push(options.offset);\r\n }\r\n\r\n return { sql, params };\r\n }\r\n\r\n private buildCountQuery(\r\n table: string,\r\n options?: QueryOptions,\r\n ): { sql: string; params: any[] } {\r\n const params: any[] = [];\r\n let sql = `SELECT COUNT(*) as count FROM ${table}`;\r\n\r\n // JOIN clauses\r\n if (options?.joins && options.joins.length > 0) {\r\n sql += this.buildJoinClause(options.joins);\r\n }\r\n\r\n // WHERE clause\r\n const whereClause = this.buildWhereClause(options, params);\r\n if (whereClause) {\r\n sql += ` WHERE ${whereClause}`;\r\n }\r\n\r\n return { sql, params };\r\n }\r\n\r\n private buildJoinClause(joins: JoinClause[]): string {\r\n return joins\r\n .map((join) => {\r\n const alias = join.alias ? ` ${join.alias}` : \"\";\r\n const leftField = join.on.leftField;\r\n const rightField = join.on.rightField;\r\n return ` ${join.type} JOIN ${join.table}${alias} ON ${leftField} = ${rightField}`;\r\n })\r\n .join(\"\");\r\n }\r\n\r\n private buildWhereClause(\r\n options: QueryOptions | undefined,\r\n params: any[],\r\n ): string {\r\n const clauses: string[] = [];\r\n\r\n // Simple where (backwards compatible)\r\n if (options?.where) {\r\n const simpleConditions = Object.entries(options.where).map(\r\n ([key, value]) => {\r\n params.push(value);\r\n return `${key} = ?`;\r\n },\r\n );\r\n if (simpleConditions.length > 0) {\r\n clauses.push(simpleConditions.join(\" AND \"));\r\n }\r\n }\r\n\r\n // Advanced where (new feature)\r\n if (options?.whereAdvanced) {\r\n const advancedClause = this.buildWhereGroupClause(\r\n options.whereAdvanced,\r\n params,\r\n );\r\n if (advancedClause) {\r\n clauses.push(advancedClause);\r\n }\r\n }\r\n\r\n return clauses.length > 0 ? clauses.join(\" AND \") : \"\";\r\n }\r\n\r\n private buildWhereGroupClause(group: WhereGroup, params: any[]): string {\r\n const conditions = group.conditions\r\n .map((condition) => {\r\n // Recursive: WhereGroup\r\n if (\"type\" in condition && \"conditions\" in condition) {\r\n return `(${this.buildWhereGroupClause(condition as WhereGroup, params)})`;\r\n }\r\n // WhereCondition\r\n return this.buildConditionClause(condition as WhereCondition, params);\r\n })\r\n .filter(Boolean);\r\n\r\n return conditions.join(` ${group.type} `);\r\n }\r\n\r\n private buildConditionClause(\r\n condition: WhereCondition,\r\n params: any[],\r\n ): string {\r\n const { field, operator, value } = condition;\r\n\r\n switch (operator) {\r\n case \"IS NULL\":\r\n return `${field} IS NULL`;\r\n\r\n case \"IS NOT NULL\":\r\n return `${field} IS NOT NULL`;\r\n\r\n case \"IN\":\r\n case \"NOT IN\":\r\n if (Array.isArray(value)) {\r\n const placeholders = value.map(() => \"?\").join(\", \");\r\n params.push(...value);\r\n return `${field} ${operator} (${placeholders})`;\r\n }\r\n return \"\";\r\n\r\n case \"BETWEEN\":\r\n case \"NOT BETWEEN\":\r\n if (Array.isArray(value) && value.length === 2) {\r\n params.push(value[0], value[1]);\r\n return `${field} ${operator} ? AND ?`;\r\n }\r\n return \"\";\r\n\r\n default:\r\n // =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE\r\n params.push(value);\r\n return `${field} ${operator} ?`;\r\n }\r\n }\r\n}\r\n"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"path": "db/react/index.ts",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"path": "db/react/useRepository.ts",
|
|
74
74
|
"type": "registry:hook",
|
|
75
75
|
"target": "$modules$/db/react/useRepository.ts",
|
|
76
|
-
"content": "import {\n useQuery,\n useMutation,\n useQueryClient,\n useInfiniteQuery,\n type UseQueryOptions,\n type UseMutationOptions,\n type UseInfiniteQueryOptions,\n} from \"@tanstack/react-query\";\nimport { DataManager } from \"../core/DataManager\";\nimport { getAdapter } from \"../config\";\nimport { queryKeys } from \"./queryClient\";\nimport type { QueryOptions } from \"../core/types\";\n\n// Singleton manager\nlet managerInstance: DataManager | null = null;\nfunction getManager() {\n if (!managerInstance) {\n managerInstance = DataManager.getInstance(getAdapter());\n }\n return managerInstance;\n}\n\n// ==========================================\n// QUERY HOOKS\n// ==========================================\n\n// Omit 'select' from QueryOptions to avoid conflict with React Query's select\nexport interface RepositoryQueryOptions<T> extends Omit<\n QueryOptions,\n \"select\"\n> {\n // SQL SELECT fields (renamed to avoid conflict)\n selectFields?: string[];\n\n // React Query options\n enabled?: boolean;\n staleTime?: number;\n gcTime?: number;\n refetchOnWindowFocus?: boolean;\n refetchInterval?: number | false;\n select?: (data: T[]) => any; // React Query data transformation\n}\n\n/**\n * Generic query hook - React Query handles all caching\n * @example\n * // Simple query\n * const { data: posts, isLoading } = useRepositoryQuery('posts', {\n * where: { published: 1 },\n * orderBy: [{ field: 'created_at', direction: 'DESC' }],\n * staleTime: 60000\n * });\n *\n * // With JOIN\n * const { data: posts } = useRepositoryQuery('posts', {\n * selectFields: ['posts.*', 'c.name as category_name'],\n * joins: [{\n * type: 'INNER',\n * table: 'post_categories',\n * alias: 'pc',\n * on: { leftField: 'posts.id', rightField: 'pc.post_id' }\n * }],\n * distinct: true\n * });\n *\n * // With complex WHERE\n * const { data: products } = useRepositoryQuery('products', {\n * whereAdvanced: {\n * type: 'AND',\n * conditions: [\n * { field: 'published', operator: '=', value: 1 },\n * { type: 'OR', conditions: [\n * { field: 'name', operator: 'LIKE', value: '%phone%' },\n * { field: 'description', operator: 'LIKE', value: '%phone%' }\n * ]}\n * ]\n * }\n * });\n */\nexport function useRepositoryQuery<T = any>(\n table: string,\n options: RepositoryQueryOptions<T> = {},\n queryOptions?: Omit<UseQueryOptions<T[], Error>, \"queryKey\" | \"queryFn\">,\n) {\n const manager = getManager();\n const {\n // QueryOptions fields\n where,\n limit,\n offset,\n orderBy,\n include,\n // New complex query fields\n selectFields,\n distinct,\n joins,\n whereAdvanced,\n groupBy,\n having,\n // React Query options\n select,\n enabled,\n staleTime,\n gcTime,\n refetchOnWindowFocus,\n refetchInterval,\n } = options;\n\n const queryOpts: QueryOptions = {\n where,\n limit,\n offset,\n orderBy,\n include,\n select: selectFields,\n distinct,\n joins,\n whereAdvanced,\n groupBy,\n having,\n };\n\n return useQuery<T[], Error>({\n queryKey: queryKeys.list(table, queryOpts),\n queryFn: () => manager.query<T>(table, queryOpts),\n select,\n enabled,\n staleTime,\n gcTime,\n refetchOnWindowFocus,\n refetchInterval,\n ...queryOptions,\n });\n}\n\n/**\n * Query single record\n * @example\n * const { data: post } = useRepositoryQueryOne('posts', {\n * where: { slug: 'my-post' }\n * });\n */\nexport function useRepositoryQueryOne<T = any>(\n table: string,\n options: RepositoryQueryOptions<T> = {},\n) {\n const result = useRepositoryQuery<T>(table, { ...options, limit: 1 });\n\n return {\n ...result,\n data: result.data?.[0] || null,\n };\n}\n\n/**\n * Query by ID - React Query caches by ID automatically\n * @example\n * const { data: post, isLoading } = useRepositoryQueryById('posts', postId);\n */\nexport function useRepositoryQueryById<T = any>(\n table: string,\n id: number | string | null | undefined,\n options: Omit<UseQueryOptions<T | null, Error>, \"queryKey\" | \"queryFn\"> = {},\n) {\n const manager = getManager();\n\n return useQuery<T | null, Error>({\n queryKey: queryKeys.detail(table, id as any),\n queryFn: () => manager.queryById<T>(table, id as any),\n enabled: options.enabled !== false && id != null,\n ...options,\n });\n}\n\n/**\n * Paginated query - React Query caches each page\n * @example\n * const { data, totalPages, hasMore } = useRepositoryPagination('products', page, 20);\n */\nexport function useRepositoryPagination<T = any>(\n table: string,\n page: number = 1,\n limit: number = 10,\n options: QueryOptions = {},\n) {\n const manager = getManager();\n\n return useQuery({\n queryKey: queryKeys.paginated(table, page, limit, options),\n queryFn: () => manager.paginate<T>(table, page, limit, options),\n });\n}\n\n/**\n * Infinite query for infinite scroll / load more\n * @example\n * const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\n * useRepositoryInfiniteQuery('posts', 20, {\n * where: { published: 1 },\n * orderBy: [{ field: 'created_at', direction: 'DESC' }]\n * });\n *\n * // data.pages = [page1Data, page2Data, page3Data, ...]\n * const allPosts = data?.pages.flatMap(page => page.data) ?? [];\n */\nexport function useRepositoryInfiniteQuery<T = any>(\n table: string,\n pageSize: number = 20,\n options: QueryOptions = {},\n queryOptions?: Omit<\n UseInfiniteQueryOptions<\n { data: T[]; page: number; totalPages: number; hasMore: boolean },\n Error\n >,\n \"queryKey\" | \"queryFn\" | \"getNextPageParam\" | \"initialPageParam\"\n >,\n) {\n const manager = getManager();\n\n return useInfiniteQuery({\n queryKey: queryKeys.infinite(table, pageSize, options),\n queryFn: ({ pageParam }) =>\n manager.paginate<T>(table, pageParam as number, pageSize, options),\n initialPageParam: 1,\n getNextPageParam: (lastPage) => {\n if (!lastPage.hasMore) return undefined;\n return lastPage.page + 1;\n },\n ...queryOptions,\n });\n}\n\n// ==========================================\n// MUTATION HOOKS (Auto-invalidation via React Query)\n// ==========================================\n\n/**\n * Create mutation - React Query handles cache invalidation\n * @example\n * const { mutate: createPost } = useRepositoryCreate('posts', {\n * onSuccess: () => toast.success('Created!')\n * });\n */\nexport function useRepositoryCreate<T = any>(\n table: string,\n options: Omit<UseMutationOptions<T, Error, Partial<T>>, \"mutationFn\"> & {\n invalidate?: string[];\n } = {},\n) {\n const manager = getManager();\n const queryClient = useQueryClient();\n const { invalidate = [table], ...mutationOptions } = options;\n\n return useMutation<T, Error, Partial<T>>({\n mutationFn: (data) => manager.create<T>(table, data),\n onSuccess: () => {\n // React Query automatically invalidates and refetches\n invalidate.forEach((t) => {\n queryClient.invalidateQueries({ queryKey: queryKeys.all(t) });\n });\n },\n ...mutationOptions,\n });\n}\n\n/**\n * Update mutation - Optimistic update via React Query\n * @example\n * const { mutate: updatePost } = useRepositoryUpdate('posts', {\n * onSuccess: () => toast.success('Updated!')\n * });\n * updatePost({ id: 1, data: { title: 'New Title' } });\n */\nexport function useRepositoryUpdate<T = any>(\n table: string,\n options: Omit<\n UseMutationOptions<T, Error, { id: number | string; data: Partial<T> }>,\n \"mutationFn\"\n > = {},\n) {\n const manager = getManager();\n const queryClient = useQueryClient();\n\n return useMutation<T, Error, { id: number | string; data: Partial<T> }>({\n mutationFn: ({ id, data }) => manager.update<T>(table, id, data),\n onSuccess: (data, variables) => {\n // Invalidate list queries\n queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n\n // Update detail cache optimistically\n queryClient.setQueryData(queryKeys.detail(table, variables.id), data);\n },\n ...options,\n });\n}\n\n/**\n * Delete mutation\n * @example\n * const { mutate: deletePost } = useRepositoryDelete('posts', {\n * onSuccess: () => toast.success('Deleted!')\n * });\n * deletePost(postId);\n */\nexport function useRepositoryDelete(\n table: string,\n options: Omit<\n UseMutationOptions<boolean, Error, number | string>,\n \"mutationFn\"\n > = {},\n) {\n const manager = getManager();\n const queryClient = useQueryClient();\n\n return useMutation<boolean, Error, number | string>({\n mutationFn: (id) => manager.delete(table, id),\n onSuccess: (_data, id) => {\n // Invalidate and remove from cache\n queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n queryClient.removeQueries({ queryKey: queryKeys.detail(table, id) });\n },\n ...options,\n });\n}\n\n// ==========================================\n// RAW SQL QUERY HOOKS\n// ==========================================\n\n/**\n * Raw SQL query hook - for complex queries that can't be expressed with QueryOptions\n * @example\n * // Complex JOIN query\n * const { data: posts } = useRawQuery<Post>(\n * ['posts-with-categories', categorySlug],\n * `SELECT DISTINCT p.*, c.name as category_name\n * FROM posts p\n * JOIN post_categories pc ON p.id = pc.post_id\n * JOIN blog_categories c ON pc.category_id = c.id\n * WHERE c.slug = ? AND p.published = 1\n * ORDER BY p.published_at DESC`,\n * [categorySlug]\n * );\n *\n * // Aggregation query\n * const { data: stats } = useRawQuery<{total: number, avg: number}>(\n * ['product-stats'],\n * `SELECT COUNT(*) as total, AVG(price) as avg FROM products WHERE published = 1`\n * );\n */\nexport function useRawQuery<T = any>(\n queryKey: any[],\n sql: string,\n params?: any[],\n options?: Omit<UseQueryOptions<T[], Error>, \"queryKey\" | \"queryFn\">,\n) {\n const manager = getManager();\n\n return useQuery<T[], Error>({\n queryKey: [\"raw\", ...queryKey],\n queryFn: () => manager.raw<T>(sql, params),\n ...options,\n });\n}\n\n/**\n * Raw SQL query hook for single result - aggregations, single lookups\n * @example\n * // Get price range\n * const { data: priceRange } = useRawQueryOne<{min: number, max: number}>(\n * ['price-range'],\n * `SELECT MIN(price) as min, MAX(price) as max FROM products WHERE published = 1`\n * );\n *\n * // Get single post with category\n * const { data: post } = useRawQueryOne<Post>(\n * ['post-detail', slug],\n * `SELECT p.*, c.name as category_name\n * FROM posts p\n * LEFT JOIN post_categories pc ON p.id = pc.post_id\n * LEFT JOIN blog_categories c ON pc.category_id = c.id\n * WHERE p.slug = ?`,\n * [slug]\n * );\n */\nexport function useRawQueryOne<T = any>(\n queryKey: any[],\n sql: string,\n params?: any[],\n options?: Omit<UseQueryOptions<T | null, Error>, \"queryKey\" | \"queryFn\">,\n) {\n const manager = getManager();\n\n return useQuery<T | null, Error>({\n queryKey: [\"raw\", ...queryKey],\n queryFn: () => manager.rawOne<T>(sql, params),\n ...options,\n });\n}\n"
|
|
76
|
+
"content": "import {\r\n useQuery,\r\n useMutation,\r\n useQueryClient,\r\n useInfiniteQuery,\r\n type UseQueryOptions,\r\n type UseMutationOptions,\r\n type UseInfiniteQueryOptions,\r\n} from \"@tanstack/react-query\";\r\nimport { DataManager } from \"../core/DataManager\";\r\nimport { getAdapter } from \"../config\";\r\nimport { queryKeys } from \"./queryClient\";\r\nimport type { QueryOptions } from \"../core/types\";\r\n\r\n// Singleton manager\r\nlet managerInstance: DataManager | null = null;\r\nfunction getManager() {\r\n if (!managerInstance) {\r\n managerInstance = DataManager.getInstance(getAdapter());\r\n }\r\n return managerInstance;\r\n}\r\n\r\n// ==========================================\r\n// QUERY HOOKS\r\n// ==========================================\r\n\r\n// Omit 'select' from QueryOptions to avoid conflict with React Query's select\r\nexport interface RepositoryQueryOptions<T> extends Omit<\r\n QueryOptions,\r\n \"select\"\r\n> {\r\n // SQL SELECT fields (renamed to avoid conflict)\r\n selectFields?: string[];\r\n\r\n // React Query options\r\n enabled?: boolean;\r\n staleTime?: number;\r\n gcTime?: number;\r\n refetchOnWindowFocus?: boolean;\r\n refetchInterval?: number | false;\r\n select?: (data: T[]) => any; // React Query data transformation\r\n}\r\n\r\n/**\r\n * Generic query hook - React Query handles all caching\r\n * @example\r\n * // Simple query\r\n * const { data: posts, isLoading } = useRepositoryQuery('posts', {\r\n * where: { published: 1 },\r\n * orderBy: [{ field: 'created_at', direction: 'DESC' }],\r\n * staleTime: 60000\r\n * });\r\n *\r\n * // With JOIN\r\n * const { data: posts } = useRepositoryQuery('posts', {\r\n * selectFields: ['posts.*', 'c.name as category_name'],\r\n * joins: [{\r\n * type: 'INNER',\r\n * table: 'post_categories',\r\n * alias: 'pc',\r\n * on: { leftField: 'posts.id', rightField: 'pc.post_id' }\r\n * }],\r\n * distinct: true\r\n * });\r\n *\r\n * // With complex WHERE\r\n * const { data: products } = useRepositoryQuery('products', {\r\n * whereAdvanced: {\r\n * type: 'AND',\r\n * conditions: [\r\n * { field: 'published', operator: '=', value: 1 },\r\n * { type: 'OR', conditions: [\r\n * { field: 'name', operator: 'LIKE', value: '%phone%' },\r\n * { field: 'description', operator: 'LIKE', value: '%phone%' }\r\n * ]}\r\n * ]\r\n * }\r\n * });\r\n */\r\nexport function useRepositoryQuery<T = any>(\r\n table: string,\r\n options: RepositoryQueryOptions<T> = {},\r\n queryOptions?: Omit<UseQueryOptions<T[], Error>, \"queryKey\" | \"queryFn\">,\r\n) {\r\n const manager = getManager();\r\n const {\r\n // QueryOptions fields\r\n where,\r\n limit,\r\n offset,\r\n orderBy,\r\n include,\r\n // New complex query fields\r\n selectFields,\r\n distinct,\r\n joins,\r\n whereAdvanced,\r\n groupBy,\r\n having,\r\n // React Query options\r\n select,\r\n enabled,\r\n staleTime,\r\n gcTime,\r\n refetchOnWindowFocus,\r\n refetchInterval,\r\n } = options;\r\n\r\n const queryOpts: QueryOptions = {\r\n where,\r\n limit,\r\n offset,\r\n orderBy,\r\n include,\r\n select: selectFields,\r\n distinct,\r\n joins,\r\n whereAdvanced,\r\n groupBy,\r\n having,\r\n };\r\n\r\n return useQuery<T[], Error>({\r\n queryKey: queryKeys.list(table, queryOpts),\r\n queryFn: () => manager.query<T>(table, queryOpts),\r\n select,\r\n enabled,\r\n staleTime,\r\n gcTime,\r\n refetchOnWindowFocus,\r\n refetchInterval,\r\n ...queryOptions,\r\n });\r\n}\r\n\r\n/**\r\n * Query single record\r\n * @example\r\n * const { data: post } = useRepositoryQueryOne('posts', {\r\n * where: { slug: 'my-post' }\r\n * });\r\n */\r\nexport function useRepositoryQueryOne<T = any>(\r\n table: string,\r\n options: RepositoryQueryOptions<T> = {},\r\n) {\r\n const result = useRepositoryQuery<T>(table, { ...options, limit: 1 });\r\n\r\n return {\r\n ...result,\r\n data: result.data?.[0] || null,\r\n };\r\n}\r\n\r\n/**\r\n * Query by ID - React Query caches by ID automatically\r\n * @example\r\n * const { data: post, isLoading } = useRepositoryQueryById('posts', postId);\r\n */\r\nexport function useRepositoryQueryById<T = any>(\r\n table: string,\r\n id: number | string | null | undefined,\r\n options: Omit<UseQueryOptions<T | null, Error>, \"queryKey\" | \"queryFn\"> = {},\r\n) {\r\n const manager = getManager();\r\n\r\n return useQuery<T | null, Error>({\r\n queryKey: queryKeys.detail(table, id as any),\r\n queryFn: () => manager.queryById<T>(table, id as any),\r\n enabled: options.enabled !== false && id != null,\r\n ...options,\r\n });\r\n}\r\n\r\n/**\r\n * Paginated query - React Query caches each page\r\n * @example\r\n * const { data, totalPages, hasMore } = useRepositoryPagination('products', page, 20);\r\n */\r\nexport function useRepositoryPagination<T = any>(\r\n table: string,\r\n page: number = 1,\r\n limit: number = 10,\r\n options: QueryOptions = {},\r\n) {\r\n const manager = getManager();\r\n\r\n return useQuery({\r\n queryKey: queryKeys.paginated(table, page, limit, options),\r\n queryFn: () => manager.paginate<T>(table, page, limit, options),\r\n });\r\n}\r\n\r\n/**\r\n * Infinite query for infinite scroll / load more\r\n * @example\r\n * const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\r\n * useRepositoryInfiniteQuery('posts', 20, {\r\n * where: { published: 1 },\r\n * orderBy: [{ field: 'created_at', direction: 'DESC' }]\r\n * });\r\n *\r\n * // data.pages = [page1Data, page2Data, page3Data, ...]\r\n * const allPosts = data?.pages.flatMap(page => page.data) ?? [];\r\n */\r\nexport function useRepositoryInfiniteQuery<T = any>(\r\n table: string,\r\n pageSize: number = 20,\r\n options: QueryOptions = {},\r\n queryOptions?: Omit<\r\n UseInfiniteQueryOptions<\r\n { data: T[]; page: number; totalPages: number; hasMore: boolean },\r\n Error\r\n >,\r\n \"queryKey\" | \"queryFn\" | \"getNextPageParam\" | \"initialPageParam\"\r\n >,\r\n) {\r\n const manager = getManager();\r\n\r\n return useInfiniteQuery({\r\n queryKey: queryKeys.infinite(table, pageSize, options),\r\n queryFn: ({ pageParam }) =>\r\n manager.paginate<T>(table, pageParam as number, pageSize, options),\r\n initialPageParam: 1,\r\n getNextPageParam: (lastPage) => {\r\n if (!lastPage.hasMore) return undefined;\r\n return lastPage.page + 1;\r\n },\r\n ...queryOptions,\r\n });\r\n}\r\n\r\n// ==========================================\r\n// MUTATION HOOKS (Auto-invalidation via React Query)\r\n// ==========================================\r\n\r\n/**\r\n * Create mutation - React Query handles cache invalidation\r\n * @example\r\n * const { mutate: createPost } = useRepositoryCreate('posts', {\r\n * onSuccess: () => toast.success('Created!')\r\n * });\r\n */\r\nexport function useRepositoryCreate<T = any>(\r\n table: string,\r\n options: Omit<UseMutationOptions<T, Error, Partial<T>>, \"mutationFn\"> & {\r\n invalidate?: string[];\r\n } = {},\r\n) {\r\n const manager = getManager();\r\n const queryClient = useQueryClient();\r\n const { invalidate = [table], ...mutationOptions } = options;\r\n\r\n return useMutation<T, Error, Partial<T>>({\r\n mutationFn: (data) => manager.create<T>(table, data),\r\n onSuccess: () => {\r\n // React Query automatically invalidates and refetches\r\n invalidate.forEach((t) => {\r\n queryClient.invalidateQueries({ queryKey: queryKeys.all(t) });\r\n });\r\n },\r\n ...mutationOptions,\r\n });\r\n}\r\n\r\n/**\r\n * Update mutation - Optimistic update via React Query\r\n * @example\r\n * const { mutate: updatePost } = useRepositoryUpdate('posts', {\r\n * onSuccess: () => toast.success('Updated!')\r\n * });\r\n * updatePost({ id: 1, data: { title: 'New Title' } });\r\n */\r\nexport function useRepositoryUpdate<T = any>(\r\n table: string,\r\n options: Omit<\r\n UseMutationOptions<T, Error, { id: number | string; data: Partial<T> }>,\r\n \"mutationFn\"\r\n > = {},\r\n) {\r\n const manager = getManager();\r\n const queryClient = useQueryClient();\r\n\r\n return useMutation<T, Error, { id: number | string; data: Partial<T> }>({\r\n mutationFn: ({ id, data }) => manager.update<T>(table, id, data),\r\n onSuccess: (data, variables) => {\r\n // Invalidate list queries\r\n queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\r\n\r\n // Update detail cache optimistically\r\n queryClient.setQueryData(queryKeys.detail(table, variables.id), data);\r\n },\r\n ...options,\r\n });\r\n}\r\n\r\n/**\r\n * Delete mutation\r\n * @example\r\n * const { mutate: deletePost } = useRepositoryDelete('posts', {\r\n * onSuccess: () => toast.success('Deleted!')\r\n * });\r\n * deletePost(postId);\r\n */\r\nexport function useRepositoryDelete(\r\n table: string,\r\n options: Omit<\r\n UseMutationOptions<boolean, Error, number | string>,\r\n \"mutationFn\"\r\n > = {},\r\n) {\r\n const manager = getManager();\r\n const queryClient = useQueryClient();\r\n\r\n return useMutation<boolean, Error, number | string>({\r\n mutationFn: (id) => manager.delete(table, id),\r\n onSuccess: (_data, id) => {\r\n // Invalidate and remove from cache\r\n queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\r\n queryClient.removeQueries({ queryKey: queryKeys.detail(table, id) });\r\n },\r\n ...options,\r\n });\r\n}\r\n\r\n// ==========================================\r\n// RAW SQL QUERY HOOKS\r\n// ==========================================\r\n\r\n/**\r\n * Raw SQL query hook - for complex queries that can't be expressed with QueryOptions\r\n * @example\r\n * // Complex JOIN query\r\n * const { data: posts } = useRawQuery<Post>(\r\n * ['posts-with-categories', categorySlug],\r\n * `SELECT DISTINCT p.*, c.name as category_name\r\n * FROM posts p\r\n * JOIN post_categories pc ON p.id = pc.post_id\r\n * JOIN blog_categories c ON pc.category_id = c.id\r\n * WHERE c.slug = ? AND p.published = 1\r\n * ORDER BY p.published_at DESC`,\r\n * [categorySlug]\r\n * );\r\n *\r\n * // Aggregation query\r\n * const { data: stats } = useRawQuery<{total: number, avg: number}>(\r\n * ['product-stats'],\r\n * `SELECT COUNT(*) as total, AVG(price) as avg FROM products WHERE published = 1`\r\n * );\r\n */\r\nexport function useRawQuery<T = any>(\r\n queryKey: any[],\r\n sql: string,\r\n params?: any[],\r\n options?: Omit<UseQueryOptions<T[], Error>, \"queryKey\" | \"queryFn\">,\r\n) {\r\n const manager = getManager();\r\n\r\n return useQuery<T[], Error>({\r\n queryKey: [\"raw\", ...queryKey],\r\n queryFn: () => manager.raw<T>(sql, params),\r\n ...options,\r\n });\r\n}\r\n\r\n/**\r\n * Raw SQL query hook for single result - aggregations, single lookups\r\n * @example\r\n * // Get price range\r\n * const { data: priceRange } = useRawQueryOne<{min: number, max: number}>(\r\n * ['price-range'],\r\n * `SELECT MIN(price) as min, MAX(price) as max FROM products WHERE published = 1`\r\n * );\r\n *\r\n * // Get single post with category\r\n * const { data: post } = useRawQueryOne<Post>(\r\n * ['post-detail', slug],\r\n * `SELECT p.*, c.name as category_name\r\n * FROM posts p\r\n * LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n * LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n * WHERE p.slug = ?`,\r\n * [slug]\r\n * );\r\n */\r\nexport function useRawQueryOne<T = any>(\r\n queryKey: any[],\r\n sql: string,\r\n params?: any[],\r\n options?: Omit<UseQueryOptions<T | null, Error>, \"queryKey\" | \"queryFn\">,\r\n) {\r\n const manager = getManager();\r\n\r\n return useQuery<T | null, Error>({\r\n queryKey: [\"raw\", ...queryKey],\r\n queryFn: () => manager.rawOne<T>(sql, params),\r\n ...options,\r\n });\r\n}\r\n"
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
"path": "db/utils/parsers.ts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Blog Core
|
|
2
2
|
|
|
3
|
-
Complete blog state management with Zustand. Includes useBlogStore for saved/favorite posts functionality,
|
|
3
|
+
Complete blog state management with Zustand. Includes useBlogStore for saved/favorite posts functionality, useDbPosts hook for fetching posts with category filtering, search, and pagination. TypeScript types for Post, Category, and Author. No provider wrapping needed.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Complete blog state management with Zustand. Includes useBlogStore for saved/fav
|
|
|
9
9
|
| `$modules$/blog-core/index.ts` | index |
|
|
10
10
|
| `$modules$/blog-core/types.ts` | type |
|
|
11
11
|
| `$modules$/blog-core/stores/blog-store.ts` | store |
|
|
12
|
-
| `$modules$/blog-core/
|
|
12
|
+
| `$modules$/blog-core/useDbPosts.ts` | hook |
|
|
13
13
|
| `$modules$/blog-core/lang/en.json` | lang |
|
|
14
14
|
| `$modules$/blog-core/lang/tr.json` | lang |
|
|
15
15
|
|
|
@@ -17,21 +17,26 @@ Complete blog state management with Zustand. Includes useBlogStore for saved/fav
|
|
|
17
17
|
|
|
18
18
|
**Types:** `Author`, `BlogCategory`, `BlogContextType`, `BlogSettings`, `Comment`, `Post`, `PostCategory`
|
|
19
19
|
|
|
20
|
-
**Components/Functions:** `useBlog`, `
|
|
20
|
+
**Components/Functions:** `useBlog`, `useBlogStore`, `useDbBlogCategories`, `useDbFeaturedPosts`, `useDbPopularPosts`, `useDbPostBySlug`, `useDbPostSearch`, `useDbPostStats`, `useDbPosts`, `useDbPostsByCategory`, `useDbPostsByTag`, `useDbRecentPosts`
|
|
21
21
|
|
|
22
22
|
```typescript
|
|
23
|
-
import { useBlog,
|
|
23
|
+
import { useBlog, useBlogStore, useDbBlogCategories, ... } from '@/modules/blog-core';
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Usage
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
import { useBlog,
|
|
29
|
+
import { useBlog, useDbPosts, useDbPostBySlug } from '@/modules/blog-core';
|
|
30
30
|
|
|
31
31
|
// No provider needed - just use the hooks:
|
|
32
32
|
const { favorites, addToFavorites, isFavorite } = useBlog();
|
|
33
|
-
const { posts, loading } =
|
|
33
|
+
const { posts, loading } = useDbPosts();
|
|
34
34
|
|
|
35
35
|
// Or use store directly with selectors:
|
|
36
36
|
const favorites = useBlogStore((s) => s.favorites);
|
|
37
37
|
```
|
|
38
|
+
|
|
39
|
+
## Dependencies
|
|
40
|
+
|
|
41
|
+
This component requires:
|
|
42
|
+
- `db`
|
|
@@ -26,7 +26,7 @@ import { BlogListPage } from '@/modules/blog-list-page';
|
|
|
26
26
|
|
|
27
27
|
<Route path="/blog" element={<BlogListPage />} />
|
|
28
28
|
|
|
29
|
-
• Uses
|
|
29
|
+
• Uses useDbPosts() from blog-core (Zustand)
|
|
30
30
|
• Features: category tabs, search, grid/list view
|
|
31
31
|
• Sidebar: popular posts, categories, newsletter
|
|
32
32
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# E-commerce Core
|
|
2
2
|
|
|
3
|
-
Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist,
|
|
3
|
+
Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist, useDbProducts hook for product fetching with filtering/sorting/pagination, and useDbSearch hook. No provider wrapping needed.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -10,8 +10,8 @@ Complete e-commerce state management with Zustand. Includes useCartStore for sho
|
|
|
10
10
|
| `$modules$/ecommerce-core/types.ts` | type |
|
|
11
11
|
| `$modules$/ecommerce-core/stores/cart-store.ts` | store |
|
|
12
12
|
| `$modules$/ecommerce-core/stores/favorites-store.ts` | store |
|
|
13
|
-
| `$modules$/ecommerce-core/
|
|
14
|
-
| `$modules$/ecommerce-core/
|
|
13
|
+
| `$modules$/ecommerce-core/useDbProducts.ts` | hook |
|
|
14
|
+
| `$modules$/ecommerce-core/useDbSearch.ts` | hook |
|
|
15
15
|
| `$modules$/ecommerce-core/format-price.ts` | lib |
|
|
16
16
|
| `$modules$/ecommerce-core/payment-config.ts` | lib |
|
|
17
17
|
| `$modules$/ecommerce-core/lang/en.json` | lang |
|
|
@@ -21,7 +21,7 @@ Complete e-commerce state management with Zustand. Includes useCartStore for sho
|
|
|
21
21
|
|
|
22
22
|
**Types:** `Address`, `CartContextType`, `CartItem`, `CartState`, `Category`, `FavoritesContextType`, `OnlinePaymentProvider`, `Order`, `OrderItem`, `PaymentMethod`, `PaymentMethodConfig`, `Product`, `ProductCategory`, `ProductVariant`, `User`
|
|
23
23
|
|
|
24
|
-
**Components/Functions:** `ONLINE_PROVIDER_CONFIGS`, `PAYMENT_METHOD_CONFIGS`, `formatPrice`, `getAvailablePaymentMethods`, `getFilteredPaymentMethodConfigs`, `getOnlinePaymentProviders`, `isOnlineProviderAvailable`, `isPaymentMethodAvailable`, `useCart`, `useCartStore`, `
|
|
24
|
+
**Components/Functions:** `ONLINE_PROVIDER_CONFIGS`, `PAYMENT_METHOD_CONFIGS`, `formatPrice`, `getAvailablePaymentMethods`, `getFilteredPaymentMethodConfigs`, `getOnlinePaymentProviders`, `isOnlineProviderAvailable`, `isPaymentMethodAvailable`, `useCart`, `useCartStore`, `useDbCategories`, `useDbFeaturedProducts`, `useDbProductBySlug`, `useDbProducts`, `useDbSearch`, `useFavorites`, `useFavoritesStore`
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
27
|
import { ONLINE_PROVIDER_CONFIGS, PAYMENT_METHOD_CONFIGS, formatPrice, ... } from '@/modules/ecommerce-core';
|
|
@@ -30,13 +30,18 @@ import { ONLINE_PROVIDER_CONFIGS, PAYMENT_METHOD_CONFIGS, formatPrice, ... } fro
|
|
|
30
30
|
## Usage
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
import { useCart, useFavorites,
|
|
33
|
+
import { useCart, useFavorites, useDbProducts } from '@/modules/ecommerce-core';
|
|
34
34
|
|
|
35
35
|
// No provider needed - just use the hooks:
|
|
36
36
|
const { addItem, removeItem, state, itemCount } = useCart();
|
|
37
37
|
const { addToFavorites, isFavorite } = useFavorites();
|
|
38
|
-
const { products, loading } =
|
|
38
|
+
const { products, loading } = useDbProducts();
|
|
39
39
|
|
|
40
40
|
// Or use stores directly with selectors:
|
|
41
41
|
const itemCount = useCartStore((s) => s.itemCount);
|
|
42
42
|
```
|
|
43
|
+
|
|
44
|
+
## Dependencies
|
|
45
|
+
|
|
46
|
+
This component requires:
|
|
47
|
+
- `db`
|
|
@@ -28,7 +28,7 @@ import { FeaturedProducts } from '@/modules/featured-products';
|
|
|
28
28
|
|
|
29
29
|
• Installed at: src/modules/featured-products/
|
|
30
30
|
• Customize content: src/modules/featured-products/lang/*.json
|
|
31
|
-
• Products auto-loaded via
|
|
31
|
+
• Products auto-loaded via useDbProducts hook
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Post Detail Page
|
|
2
2
|
|
|
3
|
-
Blog post detail page that fetches post data by slug from URL params. Uses
|
|
3
|
+
Blog post detail page that fetches post data by slug from URL params. Uses useDbPostBySlug hook from blog-core and renders PostDetailBlock. Includes loading skeleton, error handling for not found posts, and automatic page title.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ import { PostDetailPage } from '@/modules/post-detail-page';
|
|
|
26
26
|
|
|
27
27
|
<Route path="/blog/:slug" element={<PostDetailPage />} />
|
|
28
28
|
|
|
29
|
-
• Uses
|
|
29
|
+
• Uses useDbPostBySlug() from blog-core
|
|
30
30
|
• Fetches post by slug from URL params
|
|
31
31
|
• Shows loading skeleton while fetching
|
|
32
32
|
• Handles post not found state
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Product Detail Page
|
|
2
2
|
|
|
3
|
-
Product detail page that fetches product data by slug from URL params. Uses
|
|
3
|
+
Product detail page that fetches product data by slug from URL params. Uses useDbProductBySlug hook from ecommerce-core and renders ProductDetailBlock. Includes loading skeleton, error handling for not found products, and automatic page title.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ import { ProductDetailPage } from '@/modules/product-detail-page';
|
|
|
26
26
|
|
|
27
27
|
<Route path="/products/:slug" element={<ProductDetailPage />} />
|
|
28
28
|
|
|
29
|
-
• Uses
|
|
29
|
+
• Uses useDbProductBySlug() from ecommerce-core
|
|
30
30
|
• Fetches product by slug from URL params
|
|
31
31
|
• Shows loading skeleton while fetching
|
|
32
32
|
• Handles product not found state
|
|
@@ -29,7 +29,7 @@ import ProductsPage from '@/modules/products-page';
|
|
|
29
29
|
• Installed at: src/modules/products-page/
|
|
30
30
|
• Add link: <Link to="/products">Browse Products</Link>
|
|
31
31
|
• Supports filters, sorting, grid/list view, pagination
|
|
32
|
-
• Uses
|
|
32
|
+
• Uses useDbProducts hook for data fetching
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Dependencies
|