@promakeai/cli 0.0.5 → 0.1.0
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 +214 -135
- package/dist/registry/about-page.json +5 -3
- package/dist/registry/about-section.json +2 -2
- package/dist/registry/announcement-bar.json +43 -0
- package/dist/registry/api.json +55 -0
- package/dist/registry/auth-core.json +43 -0
- package/dist/registry/auth.json +70 -0
- package/dist/registry/bento-grid-section.json +1 -1
- package/dist/registry/blog-list-page.json +3 -2
- package/dist/registry/blog-section.json +2 -2
- package/dist/registry/cart-drawer.json +1 -1
- package/dist/registry/cart-page.json +5 -4
- package/dist/registry/case-study-page.json +48 -0
- package/dist/registry/category-section.json +1 -1
- package/dist/registry/checkout-page.json +7 -5
- package/dist/registry/coming-soon-page-minimal.json +45 -0
- package/dist/registry/coming-soon-page.json +47 -0
- package/dist/registry/contact-info-grid.json +2 -2
- package/dist/registry/contact-page-centered.json +2 -2
- package/dist/registry/contact-page-map-overlay.json +4 -3
- package/dist/registry/contact-page-map-split.json +4 -3
- package/dist/registry/contact-page-split.json +3 -3
- package/dist/registry/contact-page.json +5 -3
- package/dist/registry/cookie-consent.json +43 -0
- package/dist/registry/cookies-page.json +4 -2
- package/dist/registry/cta-section.json +2 -2
- package/dist/registry/db.json +129 -0
- package/dist/registry/docs/about-page.md +5 -0
- package/dist/registry/docs/announcement-bar.md +38 -0
- package/dist/registry/docs/auth-core.md +64 -0
- package/dist/registry/docs/blog-list-page.md +1 -0
- package/dist/registry/docs/cart-page.md +1 -0
- package/dist/registry/docs/case-study-page.md +39 -0
- package/dist/registry/docs/checkout-page.md +3 -1
- package/dist/registry/docs/coming-soon-page-minimal.md +32 -0
- package/dist/registry/docs/coming-soon-page.md +37 -0
- package/dist/registry/docs/contact-page-map-overlay.md +2 -2
- package/dist/registry/docs/contact-page-map-split.md +2 -2
- package/dist/registry/docs/contact-page.md +5 -0
- package/dist/registry/docs/cookie-consent.md +37 -0
- package/dist/registry/docs/cookies-page.md +5 -0
- package/dist/registry/docs/ecommerce-core.md +4 -3
- package/dist/registry/docs/forgot-password-page-split.md +45 -0
- package/dist/registry/docs/forgot-password-page.md +46 -0
- package/dist/registry/docs/header-ecommerce.md +2 -0
- package/dist/registry/docs/hero-carousel.md +37 -0
- package/dist/registry/docs/landing-page-app.md +43 -0
- package/dist/registry/docs/landing-page-saas.md +41 -0
- package/dist/registry/docs/login-page-split.md +13 -4
- package/dist/registry/docs/login-page.md +17 -4
- package/dist/registry/docs/logo-cloud.md +33 -0
- package/dist/registry/docs/masonry-grid.md +37 -0
- package/dist/registry/docs/my-orders-page.md +44 -0
- package/dist/registry/docs/order-confirmation-page.md +41 -0
- package/dist/registry/docs/portfolio-page.md +38 -0
- package/dist/registry/docs/pricing-page.md +38 -0
- package/dist/registry/docs/privacy-page.md +5 -0
- package/dist/registry/docs/product-quick-view.md +37 -0
- package/dist/registry/docs/products-page.md +1 -0
- package/dist/registry/docs/reading-progress.md +31 -0
- package/dist/registry/docs/register-page-split.md +45 -0
- package/dist/registry/docs/register-page.md +46 -0
- package/dist/registry/docs/reset-password-page-split.md +45 -0
- package/dist/registry/docs/reset-password-page.md +36 -0
- package/dist/registry/docs/share-buttons.md +37 -0
- package/dist/registry/docs/team-page.md +38 -0
- package/dist/registry/docs/terms-page.md +5 -0
- package/dist/registry/docs/timeline-section.md +37 -0
- package/dist/registry/docs/video-hero.md +41 -0
- package/dist/registry/ecommerce-core.json +18 -2
- package/dist/registry/empty-page.json +1 -1
- package/dist/registry/faq-categorized.json +2 -2
- package/dist/registry/faq-simple.json +2 -2
- package/dist/registry/favorites-blog-block.json +1 -1
- package/dist/registry/favorites-ecommerce-block.json +1 -1
- package/dist/registry/feature-section.json +2 -2
- package/dist/registry/featured-products.json +1 -1
- package/dist/registry/footer-detailed.json +1 -1
- package/dist/registry/footer-minimal.json +3 -3
- package/dist/registry/footer.json +2 -2
- package/dist/registry/forgot-password-page-split.json +50 -0
- package/dist/registry/forgot-password-page.json +51 -0
- package/dist/registry/header-ecommerce.json +4 -2
- package/dist/registry/header-mega.json +2 -2
- package/dist/registry/header-minimal.json +1 -1
- package/dist/registry/header-simple.json +1 -1
- package/dist/registry/hero-carousel.json +45 -0
- package/dist/registry/hero-cta.json +2 -2
- package/dist/registry/hero-gradient.json +2 -2
- package/dist/registry/hero-profile.json +1 -1
- package/dist/registry/hero.json +2 -2
- package/dist/registry/index.json +24 -1
- package/dist/registry/landing-page-app.json +47 -0
- package/dist/registry/landing-page-saas.json +47 -0
- package/dist/registry/login-page-split.json +11 -7
- package/dist/registry/login-page.json +4 -4
- package/dist/registry/logo-cloud.json +41 -0
- package/dist/registry/masonry-grid.json +43 -0
- package/dist/registry/my-orders-page.json +52 -0
- package/dist/registry/order-confirmation-page.json +49 -0
- package/dist/registry/orders-list-block.json +1 -1
- package/dist/registry/payment-success-block.json +1 -1
- package/dist/registry/portfolio-page.json +45 -0
- package/dist/registry/post-detail-block.json +1 -1
- package/dist/registry/pricing-page.json +47 -0
- package/dist/registry/pricing-section.json +2 -2
- package/dist/registry/privacy-page.json +4 -2
- package/dist/registry/product-detail-block.json +1 -1
- package/dist/registry/product-quick-view.json +46 -0
- package/dist/registry/products-page.json +5 -4
- package/dist/registry/reading-progress.json +43 -0
- package/dist/registry/register-page-split.json +50 -0
- package/dist/registry/register-page.json +51 -0
- package/dist/registry/related-posts-block.json +1 -1
- package/dist/registry/reset-password-page-split.json +50 -0
- package/dist/registry/reset-password-page.json +39 -0
- package/dist/registry/share-buttons.json +46 -0
- package/dist/registry/team-page.json +47 -0
- package/dist/registry/terms-page.json +4 -2
- package/dist/registry/testimonials-carousel.json +2 -2
- package/dist/registry/testimonials-grid.json +2 -2
- package/dist/registry/timeline-section.json +43 -0
- package/dist/registry/video-hero.json +42 -0
- package/package.json +1 -1
- package/template/index.html +5 -5
- package/template/src/App.tsx +7 -24
- package/template/src/components/GoogleAnalytics.tsx +34 -0
- package/template/src/components/Layout.tsx +1 -5
- package/template/src/components/ScriptInjector.tsx +62 -0
- package/template/src/constants/constants.json +8 -2
- package/template/src/index.css +1 -0
- package/template/src/lang/en/index.json +1 -28
- package/template/src/lang/tr/index.json +1 -28
- package/template/src/pages/Index.tsx +1 -98
- package/template/src/components/Footer.tsx +0 -100
- package/template/src/components/Header.tsx +0 -79
- package/template/src/components/Hero.tsx +0 -69
- package/template/src/modules/api/USAGE.md +0 -515
- package/template/src/modules/api/customer-client.ts +0 -20
- package/template/src/modules/api/get-error-message.ts +0 -18
- package/template/src/modules/api/validation/en.json +0 -29
- package/template/src/modules/api/validation/tr.json +0 -29
- package/template/src/modules/auth/USAGE.md +0 -248
- package/template/src/modules/auth/auth-header-menu.tsx +0 -123
- package/template/src/modules/auth/auth-store.ts +0 -57
- package/template/src/modules/auth/forgot-password-page.tsx +0 -371
- package/template/src/modules/auth/login-page.tsx +0 -183
- package/template/src/modules/auth/register-page.tsx +0 -252
- package/template/src/modules/auth/use-auth.ts +0 -273
- package/template/src/modules/db/adapters/IDataAdapter.ts +0 -26
- package/template/src/modules/db/adapters/SqliteAdapter.ts +0 -364
- package/template/src/modules/db/adapters/index.ts +0 -2
- package/template/src/modules/db/config.ts +0 -59
- package/template/src/modules/db/core/DataManager.ts +0 -125
- package/template/src/modules/db/core/types.ts +0 -101
- package/template/src/modules/db/index.ts +0 -42
- package/template/src/modules/db/react/QueryProvider.tsx +0 -16
- package/template/src/modules/db/react/index.ts +0 -23
- package/template/src/modules/db/react/queryClient.ts +0 -64
- package/template/src/modules/db/react/useRepository.ts +0 -400
- package/template/src/modules/db/utils/parsers.ts +0 -96
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"path": "payment-success-block/payment-success-block.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
18
|
"target": "$modules$/payment-success-block/payment-success-block.tsx",
|
|
19
|
-
"content": "import { Link } from \"react-router\";\nimport {\n CheckCircle,\n XCircle,\n Loader2,\n ShoppingBag,\n Package,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { useTranslation } from \"react-i18next\";\n\ntype PaymentStatus = \"loading\" | \"success\" | \"failed\";\n\ninterface OrderDetails {\n id?: string;\n totalAmount?: number;\n currency?: string;\n paymentMethod?: string;\n paymentStatus?: string;\n status?: string;\n}\n\ninterface PaymentSuccessBlockProps {\n status: PaymentStatus;\n orderDetails?: OrderDetails | null;\n errorMessage?: string;\n onRetry?: () => void;\n}\n\nexport function PaymentSuccessBlock({\n status,\n orderDetails,\n errorMessage,\n onRetry,\n}: PaymentSuccessBlockProps) {\n const { t } = useTranslation(\"payment-success-block\");\n\n // Loading State\n if (status === \"loading\") {\n return (\n <div className=\"container mx-auto px-4 py-16\">\n <div className=\"max-w-md mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <Loader2 className=\"w-16 h-16 text-primary mx-auto mb-4 animate-spin\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"verifyingPayment\", \"Verifying Payment\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\n \"pleaseWait\",\n \"Please wait while we verify your payment...\"\n )}\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n // Failed State\n if (status === \"failed\") {\n return (\n <div className=\"container mx-auto px-4 py-16\">\n <div className=\"max-w-md mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <XCircle className=\"w-16 h-16 text-destructive mx-auto mb-4\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"paymentFailed\", \"Payment Failed\")}\n </h1>\n\n {/* Error Message */}\n {errorMessage && (\n <div className=\"bg-destructive/10 border border-destructive/30 rounded-lg p-4 mb-4\">\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\n </div>\n )}\n\n <p className=\"text-muted-foreground mb-6\">\n {t(\n \"paymentFailedDescription\",\n \"We couldn't verify your payment. Please try again or contact support.\"\n )}\n </p>\n\n <div className=\"flex flex-col gap-3\">\n {onRetry && (\n <Button onClick={onRetry}>\n {t(\"tryAgain\", \"Try Again\")}\n </Button>\n )}\n <Button variant=\"outline\" asChild>\n <Link to=\"/contact\">\n {t(\"contactSupport\", \"Contact Support\")}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n // Success State\n return (\n <div className=\"container mx-auto px-4 py-16\">\n <div className=\"max-w-lg mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <CheckCircle className=\"w-20 h-20 text-green-600 dark:text-green-400 mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold mb-2\">\n {t(\"thankYou\", \"Thank You!\")}\n </h1>\n <p className=\"text-xl text-muted-foreground mb-6\">\n {t(\"orderConfirmed\", \"Your order has been confirmed.\")}\n </p>\n\n {orderDetails && (\n <div className=\"bg-muted/50 rounded-lg p-4 mb-6 text-left\">\n <h3 className=\"font-semibold mb-2\">\n {t(\"orderDetails\", \"Order Details\")}\n </h3>\n <div className=\"text-sm space-y-1\">\n {orderDetails.id && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"orderId\", \"Order ID\")}:\n </span>{\" \"}\n <span className=\"font-medium\">{orderDetails.id}</span>\n </p>\n )}\n {orderDetails.totalAmount !== undefined && orderDetails.currency && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"total\", \"Total\")}:\n </span>{\" \"}\n <span className=\"font-medium\">\n {orderDetails.currency.toUpperCase()}{\" \"}\n {orderDetails.totalAmount.toFixed(2)}\n </span>\n </p>\n )}\n {orderDetails.paymentMethod && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"paymentMethod\", \"Payment Method\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize\">\n {orderDetails.paymentMethod}\n </span>\n </p>\n )}\n {orderDetails.paymentStatus && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"paymentStatus\", \"Payment Status\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize text-green-600 dark:text-green-400\">\n {orderDetails.paymentStatus}\n </span>\n </p>\n )}\n {orderDetails.status && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"orderStatus\", \"Order Status\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize\">\n {orderDetails.status}\n </span>\n </p>\n )}\n </div>\n </div>\n )}\n\n <p className=\"text-sm text-muted-foreground mb-6\">\n {t(\n \"confirmationEmailSent\",\n \"A confirmation email has been sent to your email address.\"\n )}\n </p>\n\n <div className=\"flex flex-col gap-3\">\n <Button asChild size=\"lg\">\n <Link to=\"/orders\">\n <Package className=\"w-4 h-4 mr-2\" />\n {t(\"viewMyOrders\", \"View My Orders\")}\n </Link>\n </Button>\n <Button variant=\"outline\" asChild>\n <Link to=\"/products\">\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"
|
|
19
|
+
"content": "import { Link } from \"react-router\";\nimport {\n CheckCircle,\n XCircle,\n Loader2,\n ShoppingBag,\n Package,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { useTranslation } from \"react-i18next\";\n\ntype PaymentStatus = \"loading\" | \"success\" | \"failed\";\n\ninterface OrderDetails {\n id?: string;\n totalAmount?: number;\n currency?: string;\n paymentMethod?: string;\n paymentStatus?: string;\n status?: string;\n}\n\ninterface PaymentSuccessBlockProps {\n status: PaymentStatus;\n orderDetails?: OrderDetails | null;\n errorMessage?: string;\n onRetry?: () => void;\n}\n\nexport function PaymentSuccessBlock({\n status,\n orderDetails,\n errorMessage,\n onRetry,\n}: PaymentSuccessBlockProps) {\n const { t } = useTranslation(\"payment-success-block\");\n\n // Loading State\n if (status === \"loading\") {\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\n <div className=\"max-w-md mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <Loader2 className=\"w-16 h-16 text-primary mx-auto mb-4 animate-spin\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"verifyingPayment\", \"Verifying Payment\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\n \"pleaseWait\",\n \"Please wait while we verify your payment...\"\n )}\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n // Failed State\n if (status === \"failed\") {\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\n <div className=\"max-w-md mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <XCircle className=\"w-16 h-16 text-destructive mx-auto mb-4\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"paymentFailed\", \"Payment Failed\")}\n </h1>\n\n {/* Error Message */}\n {errorMessage && (\n <div className=\"bg-destructive/10 border border-destructive/30 rounded-lg p-4 mb-4\">\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\n </div>\n )}\n\n <p className=\"text-muted-foreground mb-6\">\n {t(\n \"paymentFailedDescription\",\n \"We couldn't verify your payment. Please try again or contact support.\"\n )}\n </p>\n\n <div className=\"flex flex-col gap-3\">\n {onRetry && (\n <Button onClick={onRetry}>\n {t(\"tryAgain\", \"Try Again\")}\n </Button>\n )}\n <Button variant=\"outline\" asChild>\n <Link to=\"/contact\">\n {t(\"contactSupport\", \"Contact Support\")}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n // Success State\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\n <div className=\"max-w-lg mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <CheckCircle className=\"w-20 h-20 text-green-600 dark:text-green-400 mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold mb-2\">\n {t(\"thankYou\", \"Thank You!\")}\n </h1>\n <p className=\"text-xl text-muted-foreground mb-6\">\n {t(\"orderConfirmed\", \"Your order has been confirmed.\")}\n </p>\n\n {orderDetails && (\n <div className=\"bg-muted/50 rounded-lg p-4 mb-6 text-left\">\n <h3 className=\"font-semibold mb-2\">\n {t(\"orderDetails\", \"Order Details\")}\n </h3>\n <div className=\"text-sm space-y-1\">\n {orderDetails.id && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"orderId\", \"Order ID\")}:\n </span>{\" \"}\n <span className=\"font-medium\">{orderDetails.id}</span>\n </p>\n )}\n {orderDetails.totalAmount !== undefined && orderDetails.currency && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"total\", \"Total\")}:\n </span>{\" \"}\n <span className=\"font-medium\">\n {orderDetails.currency.toUpperCase()}{\" \"}\n {orderDetails.totalAmount.toFixed(2)}\n </span>\n </p>\n )}\n {orderDetails.paymentMethod && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"paymentMethod\", \"Payment Method\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize\">\n {orderDetails.paymentMethod}\n </span>\n </p>\n )}\n {orderDetails.paymentStatus && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"paymentStatus\", \"Payment Status\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize text-green-600 dark:text-green-400\">\n {orderDetails.paymentStatus}\n </span>\n </p>\n )}\n {orderDetails.status && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"orderStatus\", \"Order Status\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize\">\n {orderDetails.status}\n </span>\n </p>\n )}\n </div>\n </div>\n )}\n\n <p className=\"text-sm text-muted-foreground mb-6\">\n {t(\n \"confirmationEmailSent\",\n \"A confirmation email has been sent to your email address.\"\n )}\n </p>\n\n <div className=\"flex flex-col gap-3\">\n <Button asChild size=\"lg\">\n <Link to=\"/orders\">\n <Package className=\"w-4 h-4 mr-2\" />\n {t(\"viewMyOrders\", \"View My Orders\")}\n </Link>\n </Button>\n <Button variant=\"outline\" asChild>\n <Link to=\"/products\">\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "payment-success-block/lang/en.json",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "portfolio-page",
|
|
3
|
+
"type": "registry:component",
|
|
4
|
+
"title": "Portfolio Page",
|
|
5
|
+
"description": "Filterable portfolio grid with project cards. Features category filtering, hover overlays, and smooth animations.",
|
|
6
|
+
"dependencies": [],
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"button",
|
|
9
|
+
"animations"
|
|
10
|
+
],
|
|
11
|
+
"usage": "import { PortfolioPage } from '@/modules/portfolio-page';\n\n<PortfolioPage />\n\n• Installed at: src/modules/portfolio-page/\n• Customize content: lang/en/portfolio-page.json\n• Props: items[], categories[], title, subtitle",
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "portfolio-page/portfolio-page.tsx",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "$modules$/portfolio-page/portfolio-page.tsx",
|
|
17
|
+
"content": "\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Link } from \"react-router\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\ninterface PortfolioItem {\r\n id: string;\r\n title: string;\r\n category: string;\r\n image: string;\r\n description?: string;\r\n link?: string;\r\n}\r\n\r\ninterface PortfolioPageProps {\r\n items?: PortfolioItem[];\r\n categories?: string[];\r\n title?: string;\r\n subtitle?: string;\r\n className?: string;\r\n}\r\n\r\nconst defaultItems: PortfolioItem[] = [\r\n {\r\n id: \"1\",\r\n title: \"E-commerce Platform Redesign\",\r\n category: \"Web Design\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Complete redesign of a major e-commerce platform\",\r\n link: \"/portfolio/1\",\r\n },\r\n {\r\n id: \"2\",\r\n title: \"Mobile Banking App\",\r\n category: \"Mobile App\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Secure and intuitive mobile banking experience\",\r\n link: \"/portfolio/2\",\r\n },\r\n {\r\n id: \"3\",\r\n title: \"Brand Identity System\",\r\n category: \"Branding\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Complete brand identity for a tech startup\",\r\n link: \"/portfolio/3\",\r\n },\r\n {\r\n id: \"4\",\r\n title: \"Healthcare Dashboard\",\r\n category: \"Web Design\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Analytics dashboard for healthcare providers\",\r\n link: \"/portfolio/4\",\r\n },\r\n {\r\n id: \"5\",\r\n title: \"Fitness Tracking App\",\r\n category: \"Mobile App\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Comprehensive fitness and wellness tracking\",\r\n link: \"/portfolio/5\",\r\n },\r\n {\r\n id: \"6\",\r\n title: \"Restaurant Chain Rebrand\",\r\n category: \"Branding\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Modern rebrand for a national restaurant chain\",\r\n link: \"/portfolio/6\",\r\n },\r\n];\r\n\r\nconst defaultCategories = [\"All\", \"Web Design\", \"Mobile App\", \"Branding\"];\r\n\r\nexport function PortfolioPage({\r\n items = defaultItems,\r\n categories = defaultCategories,\r\n title,\r\n subtitle,\r\n className,\r\n}: PortfolioPageProps) {\r\n const { t } = useTranslation(\"portfolio-page\");\r\n usePageTitle({ title: t(\"pageTitle\") });\r\n const [activeCategory, setActiveCategory] = useState(\"All\");\r\n\r\n const displayTitle = title ?? t(\"title\");\r\n const displaySubtitle = subtitle ?? t(\"subtitle\");\r\n\r\n const filteredItems =\r\n activeCategory === \"All\"\r\n ? items\r\n : items.filter((item) => item.category === activeCategory);\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen py-12 md:py-16\", className)}>\r\n <div className=\"container max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <FadeIn className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl md:text-5xl font-bold mb-4\">{displayTitle}</h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\r\n {displaySubtitle}\r\n </p>\r\n </FadeIn>\r\n\r\n {/* Category Filter */}\r\n <FadeIn delay={0.1} className=\"flex flex-wrap justify-center gap-2 mb-12\">\r\n {categories.map((category) => (\r\n <Button\r\n key={category}\r\n variant={activeCategory === category ? \"default\" : \"outline\"}\r\n size=\"sm\"\r\n onClick={() => setActiveCategory(category)}\r\n className=\"rounded-full\"\r\n >\r\n {category}\r\n </Button>\r\n ))}\r\n </FadeIn>\r\n\r\n {/* Portfolio Grid */}\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\r\n {filteredItems.map((item, index) => (\r\n <FadeIn key={item.id} delay={index * 0.05}>\r\n <Link\r\n to={item.link || `/portfolio/${item.id}`}\r\n className=\"group block\"\r\n >\r\n <div className=\"relative overflow-hidden rounded-xl bg-muted aspect-[4/3]\">\r\n <img\r\n src={item.image}\r\n alt={item.title}\r\n className=\"w-full h-full object-cover transition-transform duration-500 group-hover:scale-110\"\r\n />\r\n {/* Overlay */}\r\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300\">\r\n <div className=\"absolute bottom-0 left-0 right-0 p-6 translate-y-4 group-hover:translate-y-0 transition-transform duration-300\">\r\n <span className=\"inline-block text-sm text-primary-foreground/80 bg-primary/80 px-3 py-1 rounded-full mb-2\">\r\n {item.category}\r\n </span>\r\n <h3 className=\"text-xl font-bold text-white mb-1\">\r\n {item.title}\r\n </h3>\r\n {item.description && (\r\n <p className=\"text-white/80 text-sm line-clamp-2\">\r\n {item.description}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Link>\r\n </FadeIn>\r\n ))}\r\n </div>\r\n\r\n {/* Empty State */}\r\n {filteredItems.length === 0 && (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground\">{t(\"noItems\")}</p>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PortfolioPage;\r\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "portfolio-page/index.ts",
|
|
21
|
+
"type": "registry:index",
|
|
22
|
+
"target": "$modules$/portfolio-page/index.ts",
|
|
23
|
+
"content": "export * from \"./portfolio-page\";\r\nexport { PortfolioPage as default } from \"./portfolio-page\";\r\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "portfolio-page/lang/en.json",
|
|
27
|
+
"type": "registry:lang",
|
|
28
|
+
"target": "$modules$/portfolio-page/lang/en.json",
|
|
29
|
+
"content": "{\r\n \"pageTitle\": \"Portfolio\",\r\n \"title\": \"Our Portfolio\",\r\n \"subtitle\": \"Ask Promake to customize these projects based on your work and case studies.\",\r\n \"noItems\": \"No projects found in this category.\"\r\n}\r\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "portfolio-page/lang/tr.json",
|
|
33
|
+
"type": "registry:lang",
|
|
34
|
+
"target": "$modules$/portfolio-page/lang/tr.json",
|
|
35
|
+
"content": "{\r\n \"pageTitle\": \"Portfolyo\",\r\n \"title\": \"Portfolyomuz\",\r\n \"subtitle\": \"Bu projeleri kendi çalışmalarınıza ve vaka çalışmalarınıza göre özelleştirmek için Promake'e sorun.\",\r\n \"noItems\": \"Bu kategoride proje bulunamadı.\"\r\n}\r\n"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
"types": [],
|
|
40
|
+
"variables": [
|
|
41
|
+
"PortfolioPage",
|
|
42
|
+
"default"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"path": "post-detail-block/post-detail-block.tsx",
|
|
19
19
|
"type": "registry:block",
|
|
20
20
|
"target": "$modules$/post-detail-block/post-detail-block.tsx",
|
|
21
|
-
"content": "import { Link } from \"react-router\";\nimport {\n Calendar,\n User,\n Eye,\n Clock,\n ArrowLeft,\n Share2,\n Heart,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface PostDetailBlockProps {\n post: Post;\n onShare?: () => void;\n onAddToFavorites?: () => void;\n isFavorite?: boolean;\n}\n\nexport function PostDetailBlock({\n post,\n onShare,\n onAddToFavorites,\n isFavorite = false,\n}: PostDetailBlockProps) {\n const { t } = useTranslation(\"post-detail-block\");\n\n const formatDate = (dateString?: string) => {\n if (!dateString) return \"\";\n return new Date(dateString).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n });\n };\n\n return (\n <div className=\"container mx-auto px-4 py-8\">\n <div className=\"max-w-4xl mx-auto\">\n {/* Back Button */}\n <div className=\"mb-6\">\n <Button variant=\"ghost\" asChild>\n <Link to=\"/blog\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"backToBlog\", \"Back to Blog\")}\n </Link>\n </Button>\n </div>\n\n {/* Post Header */}\n <div className=\"mb-8\">\n {/* Category Badge */}\n {post.category && (\n <Badge variant=\"secondary\" className=\"mb-4\">\n {post.category}\n </Badge>\n )}\n\n {/* Title */}\n <h1 className=\"text-4xl md:text-5xl font-bold mb-6\">{post.title}</h1>\n\n {/* Meta Information */}\n <div className=\"flex flex-wrap items-center gap-4 text-muted-foreground mb-6\">\n {post.author && (\n <div className=\"flex items-center gap-2\">\n {post.author_avatar ? (\n <Avatar className=\"w-8 h-8\">\n <AvatarImage src={post.author_avatar} alt={post.author} />\n <AvatarFallback>\n {post.author\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n ) : (\n <User className=\"w-4 h-4\" />\n )}\n <span className=\"font-medium\">{post.author}</span>\n </div>\n )}\n\n {post.published_at && (\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"w-4 h-4\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n )}\n\n {post.read_time && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"w-4 h-4\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min read\")}\n </span>\n </div>\n )}\n\n {post.view_count !== undefined && (\n <div className=\"flex items-center gap-1\">\n <Eye className=\"w-4 h-4\" />\n <span>\n {post.view_count} {t(\"views\", \"views\")}\n </span>\n </div>\n )}\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-2\">\n {onShare && (\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\n <Share2 className=\"w-4 h-4 mr-2\" />\n {t(\"share\", \"Share\")}\n </Button>\n )}\n {onAddToFavorites && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={onAddToFavorites}\n className={isFavorite ? \"text-red-500\" : \"\"}\n >\n <Heart\n className={`w-4 h-4 mr-2 ${isFavorite ? \"fill-current\" : \"\"}`}\n />\n {isFavorite\n ? t(\"removeFromFavorites\", \"Remove\")\n : t(\"addToFavorites\", \"Add to Favorites\")}\n </Button>\n )}\n </div>\n </div>\n\n {/* Featured Image */}\n {post.featured_image && (\n <div className=\"mb-8\">\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-auto rounded-lg\"\n />\n </div>\n )}\n\n <Separator className=\"my-8\" />\n\n {/* Post Content */}\n <article className=\"prose prose-lg dark:prose-invert max-w-none\">\n <div\n dangerouslySetInnerHTML={{ __html: post.content }}\n className=\"leading-relaxed\"\n />\n </article>\n\n {/* Tags */}\n {post.tags && post.tags.length > 0 && (\n <div className=\"mt-8\">\n <Separator className=\"mb-4\" />\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"text-sm font-medium text-muted-foreground\">\n {t(\"tags\", \"Tags\")}:\n </span>\n {post.tags.map((tag, index) => (\n <Badge key={index} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Author Box */}\n {post.author && (\n <div className=\"mt-12 p-6 bg-muted/30 rounded-lg\">\n <div className=\"flex items-start gap-4\">\n {post.author_avatar && (\n <Avatar className=\"w-16 h-16\">\n <AvatarImage src={post.author_avatar} alt={post.author} />\n <AvatarFallback>\n {post.author\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n )}\n <div>\n <h3 className=\"font-semibold text-lg mb-1\">{post.author}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"authorDescription\", \"Author of this post\")}\n </p>\n </div>\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n"
|
|
21
|
+
"content": "import { Link } from \"react-router\";\nimport {\n Calendar,\n User,\n Eye,\n Clock,\n ArrowLeft,\n Share2,\n Heart,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface PostDetailBlockProps {\n post: Post;\n onShare?: () => void;\n onAddToFavorites?: () => void;\n isFavorite?: boolean;\n}\n\nexport function PostDetailBlock({\n post,\n onShare,\n onAddToFavorites,\n isFavorite = false,\n}: PostDetailBlockProps) {\n const { t } = useTranslation(\"post-detail-block\");\n\n const formatDate = (dateString?: string) => {\n if (!dateString) return \"\";\n return new Date(dateString).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n });\n };\n\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-4xl mx-auto\">\n {/* Back Button */}\n <div className=\"mb-6\">\n <Button variant=\"ghost\" asChild>\n <Link to=\"/blog\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"backToBlog\", \"Back to Blog\")}\n </Link>\n </Button>\n </div>\n\n {/* Post Header */}\n <div className=\"mb-8\">\n {/* Category Badge */}\n {post.category && (\n <Badge variant=\"secondary\" className=\"mb-4\">\n {post.category}\n </Badge>\n )}\n\n {/* Title */}\n <h1 className=\"text-4xl md:text-5xl font-bold mb-6\">{post.title}</h1>\n\n {/* Meta Information */}\n <div className=\"flex flex-wrap items-center gap-4 text-muted-foreground mb-6\">\n {post.author && (\n <div className=\"flex items-center gap-2\">\n {post.author_avatar ? (\n <Avatar className=\"w-8 h-8\">\n <AvatarImage src={post.author_avatar} alt={post.author} />\n <AvatarFallback>\n {post.author\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n ) : (\n <User className=\"w-4 h-4\" />\n )}\n <span className=\"font-medium\">{post.author}</span>\n </div>\n )}\n\n {post.published_at && (\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"w-4 h-4\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n )}\n\n {post.read_time && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"w-4 h-4\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min read\")}\n </span>\n </div>\n )}\n\n {post.view_count !== undefined && (\n <div className=\"flex items-center gap-1\">\n <Eye className=\"w-4 h-4\" />\n <span>\n {post.view_count} {t(\"views\", \"views\")}\n </span>\n </div>\n )}\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-2\">\n {onShare && (\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\n <Share2 className=\"w-4 h-4 mr-2\" />\n {t(\"share\", \"Share\")}\n </Button>\n )}\n {onAddToFavorites && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={onAddToFavorites}\n className={isFavorite ? \"text-red-500\" : \"\"}\n >\n <Heart\n className={`w-4 h-4 mr-2 ${isFavorite ? \"fill-current\" : \"\"}`}\n />\n {isFavorite\n ? t(\"removeFromFavorites\", \"Remove\")\n : t(\"addToFavorites\", \"Add to Favorites\")}\n </Button>\n )}\n </div>\n </div>\n\n {/* Featured Image */}\n {post.featured_image && (\n <div className=\"mb-8\">\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-auto rounded-lg\"\n />\n </div>\n )}\n\n <Separator className=\"my-8\" />\n\n {/* Post Content */}\n <article className=\"prose prose-lg dark:prose-invert max-w-none\">\n <div\n dangerouslySetInnerHTML={{ __html: post.content }}\n className=\"leading-relaxed\"\n />\n </article>\n\n {/* Tags */}\n {post.tags && post.tags.length > 0 && (\n <div className=\"mt-8\">\n <Separator className=\"mb-4\" />\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"text-sm font-medium text-muted-foreground\">\n {t(\"tags\", \"Tags\")}:\n </span>\n {post.tags.map((tag, index) => (\n <Badge key={index} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Author Box */}\n {post.author && (\n <div className=\"mt-12 p-6 bg-muted/30 rounded-lg\">\n <div className=\"flex items-start gap-4\">\n {post.author_avatar && (\n <Avatar className=\"w-16 h-16\">\n <AvatarImage src={post.author_avatar} alt={post.author} />\n <AvatarFallback>\n {post.author\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n )}\n <div>\n <h3 className=\"font-semibold text-lg mb-1\">{post.author}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"authorDescription\", \"Author of this post\")}\n </p>\n </div>\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "post-detail-block/lang/en.json",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pricing-page",
|
|
3
|
+
"type": "registry:page",
|
|
4
|
+
"title": "Pricing Page",
|
|
5
|
+
"description": "Full pricing page with 3-tier pricing cards (Free, Pro, Enterprise), monthly/annual toggle with 20% savings, feature comparison table, and FAQ teaser. Includes popular badge and staggered animations.",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"animations"
|
|
8
|
+
],
|
|
9
|
+
"route": {
|
|
10
|
+
"path": "/pricing",
|
|
11
|
+
"componentName": "PricingPage"
|
|
12
|
+
},
|
|
13
|
+
"usage": "import { PricingPage } from '@/modules/pricing-page';\n\n<Route path=\"/pricing\" element={<PricingPage />} />\n\n• Monthly/Annual billing toggle\n• Feature comparison table\n• Popular plan highlighting\n• FAQ and contact CTA",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "pricing-page/index.ts",
|
|
17
|
+
"type": "registry:index",
|
|
18
|
+
"target": "$modules$/pricing-page/index.ts",
|
|
19
|
+
"content": "export * from \"./pricing-page\";\r\nexport { default } from \"./pricing-page\";\r\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "pricing-page/pricing-page.tsx",
|
|
23
|
+
"type": "registry:page",
|
|
24
|
+
"target": "$modules$/pricing-page/pricing-page.tsx",
|
|
25
|
+
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Card, CardContent, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Switch } from \"@/components/ui/switch\";\r\nimport { Check, X, HelpCircle } from \"lucide-react\";\r\nimport { FadeIn, StaggerContainer, StaggerItem } from \"@/modules/animations\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\n\r\ninterface PricingPageProps {\r\n className?: string;\r\n}\r\n\r\nexport function PricingPage({ className }: PricingPageProps) {\r\n const { t } = useTranslation(\"pricing-page\");\r\n usePageTitle({ title: t(\"title\") });\r\n const [isAnnual, setIsAnnual] = useState(true);\r\n\r\n const plans = [\r\n {\r\n id: \"free\",\r\n name: t(\"freeName\"),\r\n description: t(\"freeDesc\"),\r\n monthlyPrice: 0,\r\n annualPrice: 0,\r\n features: [\r\n { name: t(\"freeFeature1\"), included: true },\r\n { name: t(\"freeFeature2\"), included: true },\r\n { name: t(\"freeFeature3\"), included: true },\r\n { name: t(\"freeFeature4\"), included: true },\r\n { name: t(\"freeFeature5\"), included: false },\r\n { name: t(\"freeFeature6\"), included: false },\r\n { name: t(\"freeFeature7\"), included: false },\r\n ],\r\n popular: false,\r\n cta: t(\"freeCta\"),\r\n },\r\n {\r\n id: \"pro\",\r\n name: t(\"proName\"),\r\n description: t(\"proDesc\"),\r\n monthlyPrice: 29,\r\n annualPrice: 24,\r\n features: [\r\n { name: t(\"proFeature1\"), included: true },\r\n { name: t(\"proFeature2\"), included: true },\r\n { name: t(\"proFeature3\"), included: true },\r\n { name: t(\"proFeature4\"), included: true },\r\n { name: t(\"proFeature5\"), included: true },\r\n { name: t(\"proFeature6\"), included: true },\r\n { name: t(\"proFeature7\"), included: true },\r\n ],\r\n popular: true,\r\n cta: t(\"proCta\"),\r\n },\r\n {\r\n id: \"enterprise\",\r\n name: t(\"enterpriseName\"),\r\n description: t(\"enterpriseDesc\"),\r\n monthlyPrice: 99,\r\n annualPrice: 79,\r\n features: [\r\n { name: t(\"enterpriseFeature1\"), included: true },\r\n { name: t(\"enterpriseFeature2\"), included: true },\r\n { name: t(\"enterpriseFeature3\"), included: true },\r\n { name: t(\"enterpriseFeature4\"), included: true },\r\n { name: t(\"enterpriseFeature5\"), included: true },\r\n { name: t(\"enterpriseFeature6\"), included: true },\r\n { name: t(\"enterpriseFeature7\"), included: true },\r\n ],\r\n popular: false,\r\n cta: t(\"enterpriseCta\"),\r\n },\r\n ];\r\n\r\n const comparisonFeatures = [\r\n { name: t(\"compProjects\"), free: \"3\", pro: t(\"unlimited\"), enterprise: t(\"unlimited\") },\r\n { name: t(\"compStorage\"), free: \"1GB\", pro: \"25GB\", enterprise: t(\"unlimited\") },\r\n { name: t(\"compUsers\"), free: \"1\", pro: \"10\", enterprise: t(\"unlimited\") },\r\n { name: t(\"compApi\"), free: \"1,000\", pro: \"100,000\", enterprise: t(\"unlimited\") },\r\n { name: t(\"compSupport\"), free: t(\"community\"), pro: t(\"email\"), enterprise: t(\"dedicated\") },\r\n ];\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen bg-muted/30 py-12 md:py-16\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <FadeIn className=\"text-center mb-12\">\r\n <p className=\"text-sm font-medium text-primary mb-2 uppercase tracking-wide\">\r\n {t(\"label\")}\r\n </p>\r\n <h1 className=\"text-4xl md:text-5xl font-bold text-foreground mb-4\">\r\n {t(\"heading\")}\r\n </h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto mb-8\">\r\n {t(\"description\")}\r\n </p>\r\n\r\n {/* Billing Toggle */}\r\n <div className=\"flex items-center justify-center gap-4\">\r\n <span className={cn(\"text-sm\", !isAnnual && \"text-foreground font-medium\")}>\r\n {t(\"monthly\")}\r\n </span>\r\n <Switch checked={isAnnual} onCheckedChange={setIsAnnual} />\r\n <span className={cn(\"text-sm\", isAnnual && \"text-foreground font-medium\")}>\r\n {t(\"annual\")}\r\n <Badge variant=\"secondary\" className=\"ml-2\">\r\n {t(\"save\")}\r\n </Badge>\r\n </span>\r\n </div>\r\n </FadeIn>\r\n\r\n {/* Pricing Cards */}\r\n <StaggerContainer className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20\">\r\n {plans.map((plan) => (\r\n <StaggerItem key={plan.id}>\r\n <Card className={cn(\r\n \"relative h-full flex flex-col\",\r\n plan.popular && \"border-primary shadow-lg ring-2 ring-primary/20\"\r\n )}>\r\n {plan.popular && (\r\n <div className=\"absolute -top-3 left-1/2 -translate-x-1/2\">\r\n <Badge className=\"bg-primary text-primary-foreground\">\r\n {t(\"mostPopular\")}\r\n </Badge>\r\n </div>\r\n )}\r\n\r\n <CardHeader className=\"text-center pt-8\">\r\n <CardTitle className=\"text-xl font-bold\">{plan.name}</CardTitle>\r\n <p className=\"text-sm text-muted-foreground\">{plan.description}</p>\r\n </CardHeader>\r\n\r\n <CardContent className=\"flex-1\">\r\n <div className=\"text-center mb-8\">\r\n <div className=\"flex items-baseline justify-center gap-1\">\r\n <span className=\"text-4xl md:text-5xl font-bold\">\r\n ${isAnnual ? plan.annualPrice : plan.monthlyPrice}\r\n </span>\r\n <span className=\"text-muted-foreground\">/{t(\"month\")}</span>\r\n </div>\r\n {isAnnual && plan.monthlyPrice > 0 && (\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"billedAnnually\")} (${plan.annualPrice * 12}/yr)\r\n </p>\r\n )}\r\n </div>\r\n\r\n <ul className=\"space-y-3\">\r\n {plan.features.map((feature, index) => (\r\n <li key={index} className=\"flex items-center gap-3\">\r\n {feature.included ? (\r\n <Check className=\"h-5 w-5 text-primary flex-shrink-0\" />\r\n ) : (\r\n <X className=\"h-5 w-5 text-muted-foreground/50 flex-shrink-0\" />\r\n )}\r\n <span className={cn(\r\n \"text-sm\",\r\n feature.included ? \"text-foreground\" : \"text-muted-foreground/50\"\r\n )}>\r\n {feature.name}\r\n </span>\r\n </li>\r\n ))}\r\n </ul>\r\n </CardContent>\r\n\r\n <CardFooter className=\"pt-4\">\r\n <Button\r\n asChild\r\n className=\"w-full\"\r\n variant={plan.popular ? \"default\" : \"outline\"}\r\n size=\"lg\"\r\n >\r\n <Link to={plan.id === \"enterprise\" ? \"/contact\" : \"/register\"}>\r\n {plan.cta}\r\n </Link>\r\n </Button>\r\n </CardFooter>\r\n </Card>\r\n </StaggerItem>\r\n ))}\r\n </StaggerContainer>\r\n\r\n {/* Comparison Table */}\r\n <FadeIn className=\"mb-16\">\r\n <h2 className=\"text-2xl md:text-3xl font-bold text-center mb-8\">\r\n {t(\"compareTitle\")}\r\n </h2>\r\n <div className=\"overflow-x-auto\">\r\n <table className=\"w-full border-collapse\">\r\n <thead>\r\n <tr className=\"border-b border-border\">\r\n <th className=\"text-left py-4 px-4 font-medium text-muted-foreground\">\r\n {t(\"feature\")}\r\n </th>\r\n {plans.map((plan) => (\r\n <th key={plan.id} className=\"text-center py-4 px-4 font-medium\">\r\n {plan.name}\r\n </th>\r\n ))}\r\n </tr>\r\n </thead>\r\n <tbody>\r\n {comparisonFeatures.map((feature, index) => (\r\n <tr key={index} className=\"border-b border-border\">\r\n <td className=\"py-4 px-4 text-sm\">{feature.name}</td>\r\n <td className=\"py-4 px-4 text-center text-sm text-muted-foreground\">{feature.free}</td>\r\n <td className=\"py-4 px-4 text-center text-sm font-medium\">{feature.pro}</td>\r\n <td className=\"py-4 px-4 text-center text-sm\">{feature.enterprise}</td>\r\n </tr>\r\n ))}\r\n </tbody>\r\n </table>\r\n </div>\r\n </FadeIn>\r\n\r\n {/* FAQ Teaser */}\r\n <FadeIn className=\"text-center\">\r\n <Card className=\"bg-card border\">\r\n <CardContent className=\"p-8\">\r\n <TooltipProvider>\r\n <Tooltip>\r\n <TooltipTrigger asChild>\r\n <HelpCircle className=\"h-8 w-8 text-primary mx-auto mb-4 cursor-help\" />\r\n </TooltipTrigger>\r\n <TooltipContent>\r\n <p>{t(\"faqTooltip\")}</p>\r\n </TooltipContent>\r\n </Tooltip>\r\n </TooltipProvider>\r\n <h3 className=\"text-xl font-semibold mb-2\">\r\n {t(\"faqTitle\")}\r\n </h3>\r\n <p className=\"text-muted-foreground mb-4\">\r\n {t(\"faqDesc\")}\r\n </p>\r\n <div className=\"flex flex-col sm:flex-row gap-3 justify-center\">\r\n <Button asChild variant=\"outline\">\r\n <Link to=\"/faq\">{t(\"viewFaq\")}</Link>\r\n </Button>\r\n <Button asChild>\r\n <Link to=\"/contact\">{t(\"contactSales\")}</Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PricingPage;\r\n"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "pricing-page/lang/en.json",
|
|
29
|
+
"type": "registry:lang",
|
|
30
|
+
"target": "$modules$/pricing-page/lang/en.json",
|
|
31
|
+
"content": "{\r\n \"title\": \"Pricing\",\r\n \"label\": \"Pricing\",\r\n \"heading\": \"Simple, Transparent Pricing\",\r\n \"description\": \"Ask Promake to customize this pricing description based on your plans and offers.\",\r\n \"monthly\": \"Monthly\",\r\n \"annual\": \"Annual\",\r\n \"save\": \"Save 20%\",\r\n \"month\": \"mo\",\r\n \"billedAnnually\": \"Billed annually\",\r\n \"mostPopular\": \"Most Popular\",\r\n \"freeName\": \"Free\",\r\n \"freeDesc\": \"Perfect for getting started\",\r\n \"freeFeature1\": \"Feature included\",\r\n \"freeFeature2\": \"Feature included\",\r\n \"freeFeature3\": \"Feature included\",\r\n \"freeFeature4\": \"Feature included\",\r\n \"freeFeature5\": \"Feature not included\",\r\n \"freeFeature6\": \"Feature not included\",\r\n \"freeFeature7\": \"Feature not included\",\r\n \"freeCta\": \"Get Started\",\r\n \"proName\": \"Professional\",\r\n \"proDesc\": \"Best for growing businesses\",\r\n \"proFeature1\": \"All Free features\",\r\n \"proFeature2\": \"Feature included\",\r\n \"proFeature3\": \"Feature included\",\r\n \"proFeature4\": \"Feature included\",\r\n \"proFeature5\": \"Feature included\",\r\n \"proFeature6\": \"Feature included\",\r\n \"proFeature7\": \"Feature included\",\r\n \"proCta\": \"Start Free Trial\",\r\n \"enterpriseName\": \"Enterprise\",\r\n \"enterpriseDesc\": \"For large organizations\",\r\n \"enterpriseFeature1\": \"All Pro features\",\r\n \"enterpriseFeature2\": \"Feature included\",\r\n \"enterpriseFeature3\": \"Feature included\",\r\n \"enterpriseFeature4\": \"Feature included\",\r\n \"enterpriseFeature5\": \"Feature included\",\r\n \"enterpriseFeature6\": \"Feature included\",\r\n \"enterpriseFeature7\": \"Feature included\",\r\n \"enterpriseCta\": \"Contact Sales\",\r\n \"compareTitle\": \"Compare Plans\",\r\n \"feature\": \"Feature\",\r\n \"compProjects\": \"Projects\",\r\n \"compStorage\": \"Storage\",\r\n \"compUsers\": \"Team members\",\r\n \"compApi\": \"API requests\",\r\n \"compSupport\": \"Support\",\r\n \"unlimited\": \"Unlimited\",\r\n \"community\": \"Community\",\r\n \"email\": \"Email\",\r\n \"dedicated\": \"Dedicated\",\r\n \"faqTooltip\": \"Click to see frequently asked questions\",\r\n \"faqTitle\": \"Have questions?\",\r\n \"faqDesc\": \"Check out our FAQ or contact our sales team for more information.\",\r\n \"viewFaq\": \"View FAQ\",\r\n \"contactSales\": \"Contact Sales\"\r\n}\r\n"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "pricing-page/lang/tr.json",
|
|
35
|
+
"type": "registry:lang",
|
|
36
|
+
"target": "$modules$/pricing-page/lang/tr.json",
|
|
37
|
+
"content": "{\r\n \"title\": \"Fiyatlandırma\",\r\n \"label\": \"Fiyatlandırma\",\r\n \"heading\": \"Basit, Şeffaf Fiyatlandırma\",\r\n \"description\": \"Size uygun planı seçin. Tüm planlar 14 günlük ücretsiz deneme içerir.\",\r\n \"monthly\": \"Aylık\",\r\n \"annual\": \"Yıllık\",\r\n \"save\": \"%20 Tasarruf\",\r\n \"month\": \"ay\",\r\n \"billedAnnually\": \"Yıllık faturalandırılır\",\r\n \"mostPopular\": \"En Popüler\",\r\n \"freeName\": \"Ücretsiz\",\r\n \"freeDesc\": \"Platformumuzu denemek için mükemmel\",\r\n \"freeFeature1\": \"3 projeye kadar\",\r\n \"freeFeature2\": \"Temel analitik\",\r\n \"freeFeature3\": \"Topluluk desteği\",\r\n \"freeFeature4\": \"1GB depolama\",\r\n \"freeFeature5\": \"API erişimi\",\r\n \"freeFeature6\": \"Özel alan adı\",\r\n \"freeFeature7\": \"Öncelikli destek\",\r\n \"freeCta\": \"Başlayın\",\r\n \"proName\": \"Profesyonel\",\r\n \"proDesc\": \"Büyüyen işletmeler için en iyisi\",\r\n \"proFeature1\": \"Sınırsız proje\",\r\n \"proFeature2\": \"Gelişmiş analitik\",\r\n \"proFeature3\": \"Öncelikli e-posta desteği\",\r\n \"proFeature4\": \"25GB depolama\",\r\n \"proFeature5\": \"API erişimi\",\r\n \"proFeature6\": \"Özel alan adı\",\r\n \"proFeature7\": \"Takım işbirliği\",\r\n \"proCta\": \"Ücretsiz Deneyin\",\r\n \"enterpriseName\": \"Kurumsal\",\r\n \"enterpriseDesc\": \"Büyük ölçekli organizasyonlar için\",\r\n \"enterpriseFeature1\": \"Pro'daki her şey\",\r\n \"enterpriseFeature2\": \"Sınırsız depolama\",\r\n \"enterpriseFeature3\": \"7/24 telefon desteği\",\r\n \"enterpriseFeature4\": \"Özel entegrasyonlar\",\r\n \"enterpriseFeature5\": \"Özel müdür\",\r\n \"enterpriseFeature6\": \"SLA garantisi\",\r\n \"enterpriseFeature7\": \"Şirket içi seçeneği\",\r\n \"enterpriseCta\": \"Satışa Başvurun\",\r\n \"compareTitle\": \"Planları Karşılaştırın\",\r\n \"feature\": \"Özellik\",\r\n \"compProjects\": \"Projeler\",\r\n \"compStorage\": \"Depolama\",\r\n \"compUsers\": \"Takım üyeleri\",\r\n \"compApi\": \"Aylık API istekleri\",\r\n \"compSupport\": \"Destek\",\r\n \"unlimited\": \"Sınırsız\",\r\n \"community\": \"Topluluk\",\r\n \"email\": \"E-posta\",\r\n \"dedicated\": \"7/24 Özel\",\r\n \"faqTooltip\": \"Sık sorulan soruları görmek için tıklayın\",\r\n \"faqTitle\": \"Sorularınız mı var?\",\r\n \"faqDesc\": \"SSS'imize göz atın veya daha fazla bilgi için satış ekibimizle iletişime geçin.\",\r\n \"viewFaq\": \"SSS'yi Görüntüle\",\r\n \"contactSales\": \"Satışa Başvurun\"\r\n}\r\n"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"exports": {
|
|
41
|
+
"types": [],
|
|
42
|
+
"variables": [
|
|
43
|
+
"PricingPage",
|
|
44
|
+
"default"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"path": "pricing-section/pricing-section.tsx",
|
|
20
20
|
"type": "registry:component",
|
|
21
21
|
"target": "$modules$/pricing-section/pricing-section.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\r\nimport { ArrowRight, Check } from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n CardTitle,\r\n} from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\n\r\ninterface PricingSectionProps {\r\n className?: string;\r\n}\r\n\r\nexport function PricingSection({ className }: PricingSectionProps) {\r\n const { t } = useTranslation(\"pricing-section\");\r\n\r\n const tiers = [\r\n {\r\n id: \"starter\",\r\n name: t(\"starterName\", \"Starter\"),\r\n description: t(\r\n \"starterDesc\",\r\n \"Perfect for individuals and small projects\"\r\n ),\r\n price: t(\"starterPrice\", \"$9\"),\r\n frequency: t(\"perMonth\", \"/month\"),\r\n features: [\r\n t(\"starterFeature1\", \"Up to 3 projects\"),\r\n t(\"starterFeature2\", \"Basic analytics\"),\r\n t(\"starterFeature3\", \"Email support\"),\r\n t(\"starterFeature4\", \"1GB storage\"),\r\n ],\r\n popular: false,\r\n },\r\n {\r\n id: \"professional\",\r\n name: t(\"proName\", \"Professional\"),\r\n description: t(\"proDesc\", \"Best for growing businesses and teams\"),\r\n price: t(\"proPrice\", \"$29\"),\r\n frequency: t(\"perMonth\", \"/month\"),\r\n features: [\r\n t(\"proFeature1\", \"Unlimited projects\"),\r\n t(\"proFeature2\", \"Advanced analytics\"),\r\n t(\"proFeature3\", \"Priority support\"),\r\n t(\"proFeature4\", \"10GB storage\"),\r\n t(\"proFeature5\", \"API access\"),\r\n ],\r\n popular: true,\r\n },\r\n {\r\n id: \"enterprise\",\r\n name: t(\"enterpriseName\", \"Enterprise\"),\r\n description: t(\r\n \"enterpriseDesc\",\r\n \"For large organizations with custom needs\"\r\n ),\r\n price: t(\"enterprisePrice\", \"$99\"),\r\n frequency: t(\"perMonth\", \"/month\"),\r\n features: [\r\n t(\"enterpriseFeature1\", \"Everything in Pro\"),\r\n t(\"enterpriseFeature2\", \"Unlimited storage\"),\r\n t(\"enterpriseFeature3\", \"24/7 phone support\"),\r\n t(\"enterpriseFeature4\", \"Custom integrations\"),\r\n t(\"enterpriseFeature5\", \"Dedicated manager\"),\r\n t(\"enterpriseFeature6\", \"SLA guarantee\"),\r\n ],\r\n popular: false,\r\n },\r\n ];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"container mx-auto px-4\">\r\n <div className=\"text-center mb-12\">\r\n <p className=\"text-sm font-medium text-primary mb-2\">\r\n {t(\"label\", \"Pricing\")}\r\n </p>\r\n <h2 className=\"text-3xl font-bold md:text-4xl lg:text-5xl mb-4\">\r\n {t(\"title\", \"Simple, transparent pricing\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\r\n \"subtitle\",\r\n \"Choose the plan that's right for you. All plans include a 14-day free trial.\"\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto\">\r\n {tiers.map((tier) => (\r\n <Card\r\n key={tier.id}\r\n className={cn(\r\n \"relative overflow-hidden\",\r\n tier.popular && \"border-primary shadow-lg scale-105\"\r\n )}\r\n >\r\n {tier.popular && (\r\n <div className=\"absolute top-0 right-0\">\r\n <Badge className=\"rounded-full\">\r\n {t(\"popular\", \"Most Popular\")}\r\n </Badge>\r\n </div>\r\n )}\r\n\r\n <CardHeader>\r\n <CardTitle className=\"text-xl font-bold\">{tier.name}</CardTitle>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {tier.description}\r\n </p>\r\n </CardHeader>\r\n\r\n <CardContent>\r\n <div className=\"flex items-baseline gap-1 mb-6\">\r\n <span className=\"text-4xl font-bold\">{tier.price}</span>\r\n <span className=\"text-muted-foreground text-sm\">\r\n {tier.frequency}\r\n </span>\r\n </div>\r\n\r\n <ul className=\"space-y-3\">\r\n {tier.features.map((feature, index) => (\r\n <li key={index} className=\"flex items-center gap-2\">\r\n <Check className=\"h-4 w-4 text-primary flex-shrink-0\" />\r\n <span className=\"text-sm text-muted-foreground\">\r\n {feature}\r\n </span>\r\n </li>\r\n ))}\r\n </ul>\r\n </CardContent>\r\n\r\n <CardFooter>\r\n <Button\r\n asChild\r\n className=\"w-full\"\r\n variant={tier.popular ? \"default\" : \"outline\"}\r\n >\r\n <Link to=\"/register\">\r\n {t(\"cta\", \"Get Started\")}\r\n <ArrowRight className=\"ml-2 h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n </CardFooter>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n <p className=\"text-center text-sm text-muted-foreground mt-8\">\r\n {t(\"guarantee\", \"30-day money-back guarantee. No questions asked.\")}\r\n </p>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
22
|
+
"content": "import { Link } from \"react-router\";\r\nimport { ArrowRight, Check } from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n CardTitle,\r\n} from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\n\r\ninterface PricingSectionProps {\r\n className?: string;\r\n}\r\n\r\nexport function PricingSection({ className }: PricingSectionProps) {\r\n const { t } = useTranslation(\"pricing-section\");\r\n\r\n const tiers = [\r\n {\r\n id: \"starter\",\r\n name: t(\"starterName\", \"Starter\"),\r\n description: t(\r\n \"starterDesc\",\r\n \"Perfect for individuals and small projects\"\r\n ),\r\n price: t(\"starterPrice\", \"$9\"),\r\n frequency: t(\"perMonth\", \"/month\"),\r\n features: [\r\n t(\"starterFeature1\", \"Up to 3 projects\"),\r\n t(\"starterFeature2\", \"Basic analytics\"),\r\n t(\"starterFeature3\", \"Email support\"),\r\n t(\"starterFeature4\", \"1GB storage\"),\r\n ],\r\n popular: false,\r\n },\r\n {\r\n id: \"professional\",\r\n name: t(\"proName\", \"Professional\"),\r\n description: t(\"proDesc\", \"Best for growing businesses and teams\"),\r\n price: t(\"proPrice\", \"$29\"),\r\n frequency: t(\"perMonth\", \"/month\"),\r\n features: [\r\n t(\"proFeature1\", \"Unlimited projects\"),\r\n t(\"proFeature2\", \"Advanced analytics\"),\r\n t(\"proFeature3\", \"Priority support\"),\r\n t(\"proFeature4\", \"10GB storage\"),\r\n t(\"proFeature5\", \"API access\"),\r\n ],\r\n popular: true,\r\n },\r\n {\r\n id: \"enterprise\",\r\n name: t(\"enterpriseName\", \"Enterprise\"),\r\n description: t(\r\n \"enterpriseDesc\",\r\n \"For large organizations with custom needs\"\r\n ),\r\n price: t(\"enterprisePrice\", \"$99\"),\r\n frequency: t(\"perMonth\", \"/month\"),\r\n features: [\r\n t(\"enterpriseFeature1\", \"Everything in Pro\"),\r\n t(\"enterpriseFeature2\", \"Unlimited storage\"),\r\n t(\"enterpriseFeature3\", \"24/7 phone support\"),\r\n t(\"enterpriseFeature4\", \"Custom integrations\"),\r\n t(\"enterpriseFeature5\", \"Dedicated manager\"),\r\n t(\"enterpriseFeature6\", \"SLA guarantee\"),\r\n ],\r\n popular: false,\r\n },\r\n ];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"text-center mb-12\">\r\n <p className=\"text-sm font-medium text-primary mb-2\">\r\n {t(\"label\", \"Pricing\")}\r\n </p>\r\n <h2 className=\"text-3xl font-bold md:text-4xl lg:text-5xl mb-4\">\r\n {t(\"title\", \"Simple, transparent pricing\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\r\n \"subtitle\",\r\n \"Choose the plan that's right for you. All plans include a 14-day free trial.\"\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto\">\r\n {tiers.map((tier) => (\r\n <Card\r\n key={tier.id}\r\n className={cn(\r\n \"relative overflow-hidden\",\r\n tier.popular && \"border-primary shadow-lg scale-105\"\r\n )}\r\n >\r\n {tier.popular && (\r\n <div className=\"absolute top-0 right-0\">\r\n <Badge className=\"rounded-full\">\r\n {t(\"popular\", \"Most Popular\")}\r\n </Badge>\r\n </div>\r\n )}\r\n\r\n <CardHeader>\r\n <CardTitle className=\"text-xl font-bold\">{tier.name}</CardTitle>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {tier.description}\r\n </p>\r\n </CardHeader>\r\n\r\n <CardContent>\r\n <div className=\"flex items-baseline gap-1 mb-6\">\r\n <span className=\"text-4xl font-bold\">{tier.price}</span>\r\n <span className=\"text-muted-foreground text-sm\">\r\n {tier.frequency}\r\n </span>\r\n </div>\r\n\r\n <ul className=\"space-y-3\">\r\n {tier.features.map((feature, index) => (\r\n <li key={index} className=\"flex items-center gap-2\">\r\n <Check className=\"h-4 w-4 text-primary flex-shrink-0\" />\r\n <span className=\"text-sm text-muted-foreground\">\r\n {feature}\r\n </span>\r\n </li>\r\n ))}\r\n </ul>\r\n </CardContent>\r\n\r\n <CardFooter>\r\n <Button\r\n asChild\r\n className=\"w-full\"\r\n variant={tier.popular ? \"default\" : \"outline\"}\r\n >\r\n <Link to=\"/register\">\r\n {t(\"cta\", \"Get Started\")}\r\n <ArrowRight className=\"ml-2 h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n </CardFooter>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n <p className=\"text-center text-sm text-muted-foreground mt-8\">\r\n {t(\"guarantee\", \"30-day money-back guarantee. No questions asked.\")}\r\n </p>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "pricing-section/lang/en.json",
|
|
26
26
|
"type": "registry:lang",
|
|
27
27
|
"target": "$modules$/pricing-section/lang/en.json",
|
|
28
|
-
"content": "{\r\n \"label\": \"Pricing\",\r\n \"title\": \"
|
|
28
|
+
"content": "{\r\n \"label\": \"Pricing\",\r\n \"title\": \"Ask Promake to customize this pricing title based on your site needs\",\r\n \"subtitle\": \"This subtitle will be replaced with pricing information relevant to your service offerings.\",\r\n \"perMonth\": \"/month\",\r\n \"popular\": \"Most Popular\",\r\n \"cta\": \"Get Started\",\r\n \"guarantee\": \"Lorem ipsum dolor sit amet. Ask Promake to customize guarantee text based on your policies.\",\r\n \"starterName\": \"Starter\",\r\n \"starterDesc\": \"Ask Promake to customize this plan description based on your pricing tiers\",\r\n \"starterPrice\": \"$9\",\r\n \"starterFeature1\": \"Replace with your starter plan feature\",\r\n \"starterFeature2\": \"This feature will be customized by Promake\",\r\n \"starterFeature3\": \"Placeholder feature text\",\r\n \"starterFeature4\": \"Ask Promake to replace this with relevant feature\",\r\n \"proName\": \"Pro\",\r\n \"proDesc\": \"This description will be customized based on your mid-tier offerings\",\r\n \"proPrice\": \"$29\",\r\n \"proFeature1\": \"Customize this plan feature\",\r\n \"proFeature2\": \"Ask Promake to generate appropriate feature description\",\r\n \"proFeature3\": \"Placeholder text for plan features\",\r\n \"proFeature4\": \"Replace with actual plan feature\",\r\n \"proFeature5\": \"Ask Promake to customize this feature based on your plan\",\r\n \"enterpriseName\": \"Advanced\",\r\n \"enterpriseDesc\": \"Ask Promake to customize this for your advanced offering\",\r\n \"enterprisePrice\": \"$99\",\r\n \"enterpriseFeature1\": \"Advanced plan feature placeholder\",\r\n \"enterpriseFeature2\": \"This will be replaced by Promake with advanced plan features\",\r\n \"enterpriseFeature3\": \"Customize advanced plan feature description\",\r\n \"enterpriseFeature4\": \"Ask Promake to generate appropriate advanced plan feature\",\r\n \"enterpriseFeature5\": \"Placeholder for advanced plan feature\",\r\n \"enterpriseFeature6\": \"Replace with an actual advanced plan feature\"\r\n}\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "pricing-section/lang/tr.json",
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"type": "registry:page",
|
|
4
4
|
"title": "Privacy Policy Page",
|
|
5
5
|
"description": "GDPR/CCPA-friendly privacy policy page with table of contents, collapsible sections, and legal text formatting. Covers data collection, usage, cookies, third-party services, user rights, and contact information. Last updated date and version tracking included. Professionally structured for compliance.",
|
|
6
|
-
"registryDependencies": [
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"animations"
|
|
8
|
+
],
|
|
7
9
|
"usage": "import { PrivacyPage } from '@/modules/privacy-page';\n\n<Route path=\"/privacy\" element={<PrivacyPage />} />\n\n• GDPR/CCPA compliant structure\n• Sections: data collection, cookies, rights, contact\n• Edit content in lang/en.json",
|
|
8
10
|
"route": {
|
|
9
11
|
"path": "/privacy",
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
"path": "privacy-page/privacy-page.tsx",
|
|
21
23
|
"type": "registry:page",
|
|
22
24
|
"target": "$modules$/privacy-page/privacy-page.tsx",
|
|
23
|
-
"content": "import { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Shield, Eye, Lock, UserCheck, Mail, FileText } from \"lucide-react\";\nimport { FadeIn, StaggerContainer, StaggerItem } from \"@/modules/animations\";\n\nexport function PrivacyPage() {\n const { t } = useTranslation(\"privacy-page\");\n usePageTitle({ title: t(\"title\") });\n\n const sections = [\n { icon: Eye, titleKey: \"collectionTitle\", contentKey: \"collectionContent\" },\n { icon: FileText, titleKey: \"usageTitle\", contentKey: \"usageContent\" },\n { icon: Lock, titleKey: \"securityTitle\", contentKey: \"securityContent\" },\n { icon: UserCheck, titleKey: \"rightsTitle\", contentKey: \"rightsContent\" },\n { icon: Shield, titleKey: \"cookiesTitle\", contentKey: \"cookiesContent\" },\n { icon: Mail, titleKey: \"contactTitle\", contentKey: \"contactContent\" },\n ];\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"container mx-auto px-4\">\n {/* Header */}\n <FadeIn className=\"text-center mb-12\">\n <div className=\"w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4\">\n <Shield className=\"w-8 h-8 text-primary\" />\n </div>\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\"lastUpdated\")}: {t(\"updateDate\")}\n </p>\n </FadeIn>\n\n {/* Introduction */}\n <FadeIn delay={0.1} className=\"max-w-4xl mx-auto mb-12\">\n <Card>\n <CardContent className=\"p-8\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {t(\"introduction\")}\n </p>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Sections */}\n <StaggerContainer className=\"max-w-4xl mx-auto space-y-6\">\n {sections.map(({ icon: Icon, titleKey, contentKey }, index) => (\n <StaggerItem key={titleKey}>\n <Card>\n <CardContent className=\"p-8\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center flex-shrink-0\">\n <Icon className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-xl font-semibold mb-3\">\n {index + 1}. {t(titleKey)}\n </h2>\n <p className=\"text-muted-foreground\">{t(contentKey)}</p>\n </div>\n </div>\n </CardContent>\n </Card>\n </StaggerItem>\n ))}\n </StaggerContainer>\n\n {/* Footer Note */}\n <FadeIn className=\"max-w-4xl mx-auto mt-12 text-center\">\n <p className=\"text-sm text-muted-foreground\">{t(\"footerNote\")}</p>\n </FadeIn>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default PrivacyPage;\n"
|
|
25
|
+
"content": "import { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Shield, Eye, Lock, UserCheck, Mail, FileText } from \"lucide-react\";\nimport { FadeIn, StaggerContainer, StaggerItem } from \"@/modules/animations\";\n\nexport function PrivacyPage() {\n const { t } = useTranslation(\"privacy-page\");\n usePageTitle({ title: t(\"title\") });\n\n const sections = [\n { icon: Eye, titleKey: \"collectionTitle\", contentKey: \"collectionContent\" },\n { icon: FileText, titleKey: \"usageTitle\", contentKey: \"usageContent\" },\n { icon: Lock, titleKey: \"securityTitle\", contentKey: \"securityContent\" },\n { icon: UserCheck, titleKey: \"rightsTitle\", contentKey: \"rightsContent\" },\n { icon: Shield, titleKey: \"cookiesTitle\", contentKey: \"cookiesContent\" },\n { icon: Mail, titleKey: \"contactTitle\", contentKey: \"contactContent\" },\n ];\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Header */}\n <FadeIn className=\"text-center mb-12\">\n <div className=\"w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4\">\n <Shield className=\"w-8 h-8 text-primary\" />\n </div>\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\"lastUpdated\")}: {t(\"updateDate\")}\n </p>\n </FadeIn>\n\n {/* Introduction */}\n <FadeIn delay={0.1} className=\"max-w-4xl mx-auto mb-12\">\n <Card>\n <CardContent className=\"p-8\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {t(\"introduction\")}\n </p>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Sections */}\n <StaggerContainer className=\"max-w-4xl mx-auto space-y-6\">\n {sections.map(({ icon: Icon, titleKey, contentKey }, index) => (\n <StaggerItem key={titleKey}>\n <Card>\n <CardContent className=\"p-8\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center flex-shrink-0\">\n <Icon className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-xl font-semibold mb-3\">\n {index + 1}. {t(titleKey)}\n </h2>\n <p className=\"text-muted-foreground\">{t(contentKey)}</p>\n </div>\n </div>\n </CardContent>\n </Card>\n </StaggerItem>\n ))}\n </StaggerContainer>\n\n {/* Footer Note */}\n <FadeIn className=\"max-w-4xl mx-auto mt-12 text-center\">\n <p className=\"text-sm text-muted-foreground\">{t(\"footerNote\")}</p>\n </FadeIn>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default PrivacyPage;\n"
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
"path": "privacy-page/lang/en.json",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"path": "product-detail-block/product-detail-block.tsx",
|
|
19
19
|
"type": "registry:block",
|
|
20
20
|
"target": "$modules$/product-detail-block/product-detail-block.tsx",
|
|
21
|
-
"content": "import { useState } from \"react\";\nimport {\n Star,\n Heart,\n Share2,\n Truck,\n RotateCcw,\n Shield,\n Plus,\n Minus,\n ChevronLeft,\n ChevronRight,\n X,\n ZoomIn,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Dialog, DialogContent, DialogClose } from \"@/components/ui/dialog\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductDetailBlockProps {\n product: Product;\n}\n\nexport function ProductDetailBlock({ product }: ProductDetailBlockProps) {\n const { t } = useTranslation(\"product-detail-block\");\n const { addItem } = useCart();\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [quantity, setQuantity] = useState(1);\n const [selectedVariant, setSelectedVariant] = useState<string>(\"\");\n const [isAdding, setIsAdding] = useState(false);\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\n const [zoomLevel, setZoomLevel] = useState(1);\n const [position, setPosition] = useState({ x: 0, y: 0 });\n const [isDragging, setIsDragging] = useState(false);\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n\n const handleAddToCart = async () => {\n if (product) {\n setIsAdding(true);\n // Add multiple items by calling addItem quantity times\n for (let i = 0; i < quantity; i++) {\n addItem(product);\n }\n\n // Show success feedback\n setTimeout(() => {\n setIsAdding(false);\n }, 1000);\n }\n };\n\n const handleQuantityChange = (change: number) => {\n const newQuantity = quantity + change;\n if (newQuantity >= 1 && newQuantity <= product.stock) {\n setQuantity(newQuantity);\n }\n };\n\n const features = [\n {\n icon: Truck,\n title: t(\"freeShipping\", \"Free Shipping\"),\n description: t(\"freeShippingDesc\", \"On orders over 50\"),\n },\n {\n icon: RotateCcw,\n title: t(\"easyReturns\", \"Easy Returns\"),\n description: t(\"easyReturnsDesc\", \"30-day return policy\"),\n },\n {\n icon: Shield,\n title: t(\"secureCheckout\", \"Secure Checkout\"),\n description: t(\"secureCheckoutDesc\", \"SSL encrypted payment\"),\n },\n ];\n\n return (\n <>\n <div className=\"grid lg:grid-cols-2 gap-12\">\n {/* Product Images */}\n <div className=\"space-y-4\">\n {/* Main Image */}\n <div className=\"aspect-square relative overflow-hidden rounded-lg bg-muted group\">\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-zoom-in\"\n onClick={() => setIsImageModalOpen(true)}\n />\n\n {/* Zoom Indicator */}\n <div className=\"absolute bottom-4 right-4 bg-black/50 text-white p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\">\n <ZoomIn className=\"w-5 h-5\" />\n </div>\n\n {/* Navigation Arrows */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute left-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n )\n }\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n )\n }\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </>\n )}\n\n {/* Badges */}\n <div className=\"absolute top-4 left-4 flex flex-col gap-2\">\n {product.on_sale && (\n <Badge variant=\"destructive\">{t(\"sale\", \"Sale\")}</Badge>\n )}\n {product.is_new && (\n <Badge variant=\"secondary\">{t(\"new\", \"New\")}</Badge>\n )}\n </div>\n </div>\n\n {/* Thumbnail Images */}\n {product.images.length > 1 && (\n <div className=\"flex gap-3 overflow-x-auto\">\n {product.images.map((image, index) => (\n <button\n key={index}\n className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-colors ${\n selectedImageIndex === index\n ? \"border-primary\"\n : \"border-transparent hover:border-muted-foreground\"\n }`}\n onClick={() => setSelectedImageIndex(index)}\n >\n <img\n src={image}\n alt={`${product.name} ${index + 1}`}\n className=\"w-full h-full object-cover\"\n />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"space-y-6\">\n {/* Header */}\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n <Badge variant=\"outline\">\n {product.category_name ||\n product.categories?.[0]?.name ||\n product.category}\n </Badge>\n {product.featured && (\n <Badge variant=\"secondary\">{t(\"featured\", \"Featured\")}</Badge>\n )}\n </div>\n <h1 className=\"text-3xl font-bold mb-4\">{product.name}</h1>\n\n {/* Rating */}\n <div className=\"flex items-center gap-4 mb-4\">\n <div className=\"flex items-center gap-1\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${\n i < Math.floor(product.rating)\n ? \"fill-current text-yellow-400\"\n : \"text-muted-foreground\"\n }`}\n />\n ))}\n <span className=\"text-sm font-medium ml-1\">\n {product.rating}\n </span>\n </div>\n </div>\n\n {/* Price */}\n <div className=\"flex items-center gap-3 mb-6\">\n <span className=\"text-3xl font-bold\">\n {formatPrice(\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price,\n constants.site.currency\n )}\n </span>\n {product.on_sale && product.sale_price && (\n <span className=\"text-xl text-muted-foreground line-through\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n </div>\n\n {/* Description */}\n <div>\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n </div>\n\n {/* Variants */}\n {product.variants && product.variants.length > 0 && (\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"size\", \"Size\")}\n </label>\n <Select\n value={selectedVariant}\n onValueChange={setSelectedVariant}\n >\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder={t(\"selectSize\", \"Select size\")} />\n </SelectTrigger>\n <SelectContent>\n {product.variants.map((variant: any) => (\n <SelectItem\n key={variant.id}\n value={variant.id}\n disabled={variant.stockQuantity === 0}\n >\n {variant.value}{\" \"}\n {variant.stockQuantity === 0 &&\n `(${t(\"outOfStock\", \"Out of Stock\")})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {/* Quantity */}\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"quantity\", \"Quantity\")}\n </label>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center border rounded-lg\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(-1)}\n disabled={quantity <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-12 text-center font-medium\">{quantity}</span>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(1)}\n disabled={quantity >= product.stock}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {product.stock} {t(\"available\", \"available\")}\n </span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"space-y-3\">\n <div className=\"flex gap-3\">\n <Button\n size=\"lg\"\n className=\"flex-1\"\n disabled={product.stock <= 0 || isAdding}\n variant=\"default\"\n onClick={handleAddToCart}\n >\n {isAdding ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"adding\", \"Adding...\")}\n </>\n ) : (\n t(\"addToCart\", \"Add to Cart\")\n )}\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Heart className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Share2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {product.stock > 0 ? (\n <p className=\"text-sm text-green-600 dark:text-green-400 font-medium\">\n ✓ {t(\"inStockReady\", \"In stock and ready to ship\")} (\n {product.stock} {t(\"available\", \"available\")})\n </p>\n ) : (\n <p className=\"text-sm text-red-600 dark:text-red-400 font-medium\">\n ✗ {t(\"currentlyOutOfStock\", \"Currently out of stock\")}\n </p>\n )}\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-3 gap-4 pt-6 border-t\">\n {features.map((feature, index) => {\n const Icon = feature.icon;\n return (\n <div key={index} className=\"text-center\">\n <Icon className=\"h-6 w-6 mx-auto mb-2 text-primary\" />\n <h4 className=\"text-sm font-medium\">{feature.title}</h4>\n <p className=\"text-xs text-muted-foreground\">\n {feature.description}\n </p>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {/* Product Details Tabs */}\n <Tabs defaultValue=\"description\" className=\"mt-16\">\n <TabsList className=\"grid w-full grid-cols-2\">\n <TabsTrigger value=\"description\">\n {t(\"description\", \"Description\")}\n </TabsTrigger>\n <TabsTrigger value=\"specifications\">\n {t(\"specifications\", \"Specifications\")}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"description\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"prose max-w-none\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n <Separator className=\"my-6\" />\n <h3 className=\"text-lg font-semibold mb-3\">\n {t(\"keyFeatures\", \"Key Features\")}\n </h3>\n <ul className=\"space-y-2\">\n {product.tags.map((tag, index) => (\n <li key={index} className=\"flex items-center gap-2\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full\"></div>\n <span className=\"capitalize\">{tag}</span>\n </li>\n ))}\n </ul>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"specifications\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n {product.specifications ? (\n <div className=\"grid gap-4\">\n {Object.entries(product.specifications).map(\n ([key, value]) => (\n <div\n key={key}\n className=\"bg-muted/30 rounded-lg p-4 flex justify-between items-center\"\n >\n <span className=\"font-medium text-foreground capitalize\">\n {key\n .replace(/_/g, \" \")\n .replace(/([A-Z])/g, \" $1\")\n .trim()}\n </span>\n <span className=\"text-muted-foreground font-mono bg-background px-3 py-1 rounded-md border\">\n {value}\n </span>\n </div>\n )\n )}\n </div>\n ) : (\n <p className=\"text-muted-foreground\">\n {t(\"noSpecifications\", \"No specifications available.\")}\n </p>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n\n {/* Image Zoom Modal */}\n <Dialog\n open={isImageModalOpen}\n onOpenChange={(open) => {\n setIsImageModalOpen(open);\n if (!open) {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }\n }}\n >\n <DialogContent className=\"max-w-[98vw] w-[98vw] h-[98vh] p-0 overflow-hidden bg-black/95\">\n <DialogClose className=\"absolute top-4 right-4 z-50 rounded-full bg-black/60 hover:bg-black/80 p-2 transition-all border border-white/10 backdrop-blur-md\">\n <X className=\"h-6 w-6 text-white\" />\n </DialogClose>\n\n <div className=\"relative w-full h-full flex items-center justify-center\">\n {/* Image Navigation */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute left-4 z-40 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronLeft className=\"h-8 w-8\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute right-4 z-40 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronRight className=\"h-8 w-8\" />\n </Button>\n </>\n )}\n\n {/* Zoom Controls */}\n <div className=\"absolute bottom-4 left-1/2 transform -translate-x-1/2 z-40 flex items-center gap-2 bg-black/60 backdrop-blur-md rounded-full px-4 py-3 border border-white/10\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-white hover:bg-white/20 h-8 w-8 p-0 rounded-full\"\n onClick={() => {\n const newZoom = Math.max(1, zoomLevel - 0.3);\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"text-white text-sm font-semibold min-w-[60px] text-center\">\n {Math.round(zoomLevel * 100)}%\n </span>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-white hover:bg-white/20 h-8 w-8 p-0 rounded-full\"\n onClick={() => setZoomLevel(Math.min(4, zoomLevel + 0.3))}\n disabled={zoomLevel >= 4}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n <div className=\"w-px h-6 bg-white/20 mx-2\" />\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-white hover:bg-white/20 text-xs px-3 h-8 rounded-full\"\n onClick={() => {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel === 1}\n >\n Reset\n </Button>\n </div>\n\n {/* Zoomable Image */}\n <div\n className=\"relative w-full h-full flex items-center justify-center overflow-hidden select-none\"\n style={{\n cursor:\n zoomLevel > 1\n ? isDragging\n ? \"grabbing\"\n : \"grab\"\n : \"zoom-in\",\n }}\n onWheel={(e) => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? -0.15 : 0.15;\n const newZoom = Math.max(1, Math.min(4, zoomLevel + delta));\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n onMouseDown={(e) => {\n if (zoomLevel > 1) {\n setIsDragging(true);\n setDragStart({\n x: e.clientX - position.x,\n y: e.clientY - position.y,\n });\n } else {\n // Double click to zoom\n if (e.detail === 2) {\n setZoomLevel(2.5);\n }\n }\n }}\n onMouseMove={(e) => {\n if (isDragging && zoomLevel > 1) {\n setPosition({\n x: e.clientX - dragStart.x,\n y: e.clientY - dragStart.y,\n });\n }\n }}\n onMouseUp={() => setIsDragging(false)}\n onMouseLeave={() => setIsDragging(false)}\n onClick={(e) => {\n if (zoomLevel === 1 && e.detail === 1) {\n // Single click to zoom in\n setZoomLevel(2.5);\n }\n }}\n >\n <div\n className=\"transition-transform duration-300 ease-out\"\n style={{\n transform: `scale(${zoomLevel}) translate(${\n position.x / zoomLevel\n }px, ${position.y / zoomLevel}px)`,\n transformOrigin: \"center center\",\n }}\n >\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"max-w-[90vw] max-h-[90vh] object-contain pointer-events-none\"\n draggable={false}\n />\n </div>\n </div>\n\n {/* Image Counter & Info */}\n <div className=\"absolute top-4 left-1/2 transform -translate-x-1/2 z-40 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n {product.images.length > 1 ? (\n <span className=\"text-white text-sm font-semibold\">\n {selectedImageIndex + 1} / {product.images.length}\n </span>\n ) : (\n <span className=\"text-white text-xs opacity-70\">\n {t(\"zoomHint\", \"Scroll to zoom • Click & drag to pan\")}\n </span>\n )}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"
|
|
21
|
+
"content": "import { useState } from \"react\";\nimport {\n Star,\n Heart,\n Share2,\n Truck,\n RotateCcw,\n Shield,\n Plus,\n Minus,\n ChevronLeft,\n ChevronRight,\n X,\n ZoomIn,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductDetailBlockProps {\n product: Product;\n}\n\nexport function ProductDetailBlock({ product }: ProductDetailBlockProps) {\n const { t } = useTranslation(\"product-detail-block\");\n const { addItem } = useCart();\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [quantity, setQuantity] = useState(1);\n const [selectedVariant, setSelectedVariant] = useState<string>(\"\");\n const [isAdding, setIsAdding] = useState(false);\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\n const [zoomLevel, setZoomLevel] = useState(1);\n const [position, setPosition] = useState({ x: 0, y: 0 });\n const [isDragging, setIsDragging] = useState(false);\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n\n const handleAddToCart = async () => {\n if (product) {\n setIsAdding(true);\n // Add multiple items by calling addItem quantity times\n for (let i = 0; i < quantity; i++) {\n addItem(product);\n }\n\n // Show success feedback\n setTimeout(() => {\n setIsAdding(false);\n }, 1000);\n }\n };\n\n const handleQuantityChange = (change: number) => {\n const newQuantity = quantity + change;\n if (newQuantity >= 1 && newQuantity <= product.stock) {\n setQuantity(newQuantity);\n }\n };\n\n const features = [\n {\n icon: Truck,\n title: t(\"freeShipping\", \"Free Shipping\"),\n description: t(\"freeShippingDesc\", \"On orders over 50\"),\n },\n {\n icon: RotateCcw,\n title: t(\"easyReturns\", \"Easy Returns\"),\n description: t(\"easyReturnsDesc\", \"30-day return policy\"),\n },\n {\n icon: Shield,\n title: t(\"secureCheckout\", \"Secure Checkout\"),\n description: t(\"secureCheckoutDesc\", \"SSL encrypted payment\"),\n },\n ];\n\n return (\n <>\n <div className=\"grid lg:grid-cols-2 gap-12\">\n {/* Product Images */}\n <div className=\"space-y-4\">\n {/* Main Image */}\n <div className=\"aspect-square relative overflow-hidden rounded-lg bg-muted group\">\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-zoom-in\"\n onClick={() => setIsImageModalOpen(true)}\n />\n\n {/* Zoom Indicator */}\n <div className=\"absolute bottom-4 right-4 bg-black/50 text-white p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\">\n <ZoomIn className=\"w-5 h-5\" />\n </div>\n\n {/* Navigation Arrows */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute left-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n )\n }\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n )\n }\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </>\n )}\n\n {/* Badges */}\n <div className=\"absolute top-4 left-4 flex flex-col gap-2\">\n {product.on_sale && (\n <Badge variant=\"destructive\">{t(\"sale\", \"Sale\")}</Badge>\n )}\n {product.is_new && (\n <Badge variant=\"secondary\">{t(\"new\", \"New\")}</Badge>\n )}\n </div>\n </div>\n\n {/* Thumbnail Images */}\n {product.images.length > 1 && (\n <div className=\"flex gap-3 overflow-x-auto\">\n {product.images.map((image, index) => (\n <button\n key={index}\n className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-colors ${\n selectedImageIndex === index\n ? \"border-primary\"\n : \"border-transparent hover:border-muted-foreground\"\n }`}\n onClick={() => setSelectedImageIndex(index)}\n >\n <img\n src={image}\n alt={`${product.name} ${index + 1}`}\n className=\"w-full h-full object-cover\"\n />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"space-y-6\">\n {/* Header */}\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n <Badge variant=\"outline\">\n {product.category_name ||\n product.categories?.[0]?.name ||\n product.category}\n </Badge>\n {product.featured && (\n <Badge variant=\"secondary\">{t(\"featured\", \"Featured\")}</Badge>\n )}\n </div>\n <h1 className=\"text-3xl font-bold mb-4\">{product.name}</h1>\n\n {/* Rating */}\n <div className=\"flex items-center gap-4 mb-4\">\n <div className=\"flex items-center gap-1\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${\n i < Math.floor(product.rating)\n ? \"fill-current text-yellow-400\"\n : \"text-muted-foreground\"\n }`}\n />\n ))}\n <span className=\"text-sm font-medium ml-1\">\n {product.rating}\n </span>\n </div>\n </div>\n\n {/* Price */}\n <div className=\"flex items-center gap-3 mb-6\">\n <span className=\"text-3xl font-bold\">\n {formatPrice(\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price,\n constants.site.currency\n )}\n </span>\n {product.on_sale && product.sale_price && (\n <span className=\"text-xl text-muted-foreground line-through\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n </div>\n\n {/* Description */}\n <div>\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n </div>\n\n {/* Variants */}\n {product.variants && product.variants.length > 0 && (\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"size\", \"Size\")}\n </label>\n <Select\n value={selectedVariant}\n onValueChange={setSelectedVariant}\n >\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder={t(\"selectSize\", \"Select size\")} />\n </SelectTrigger>\n <SelectContent>\n {product.variants.map((variant: any) => (\n <SelectItem\n key={variant.id}\n value={variant.id}\n disabled={variant.stockQuantity === 0}\n >\n {variant.value}{\" \"}\n {variant.stockQuantity === 0 &&\n `(${t(\"outOfStock\", \"Out of Stock\")})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {/* Quantity */}\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"quantity\", \"Quantity\")}\n </label>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center border rounded-lg\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(-1)}\n disabled={quantity <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-12 text-center font-medium\">{quantity}</span>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(1)}\n disabled={quantity >= product.stock}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {product.stock} {t(\"available\", \"available\")}\n </span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"space-y-3\">\n <div className=\"flex gap-3\">\n <Button\n size=\"lg\"\n className=\"flex-1\"\n disabled={product.stock <= 0 || isAdding}\n variant=\"default\"\n onClick={handleAddToCart}\n >\n {isAdding ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"adding\", \"Adding...\")}\n </>\n ) : (\n t(\"addToCart\", \"Add to Cart\")\n )}\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Heart className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Share2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {product.stock > 0 ? (\n <p className=\"text-sm text-green-600 dark:text-green-400 font-medium\">\n ✓ {t(\"inStockReady\", \"In stock and ready to ship\")} (\n {product.stock} {t(\"available\", \"available\")})\n </p>\n ) : (\n <p className=\"text-sm text-red-600 dark:text-red-400 font-medium\">\n ✗ {t(\"currentlyOutOfStock\", \"Currently out of stock\")}\n </p>\n )}\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-3 gap-4 pt-6 border-t\">\n {features.map((feature, index) => {\n const Icon = feature.icon;\n return (\n <div key={index} className=\"text-center\">\n <Icon className=\"h-6 w-6 mx-auto mb-2 text-primary\" />\n <h4 className=\"text-sm font-medium\">{feature.title}</h4>\n <p className=\"text-xs text-muted-foreground\">\n {feature.description}\n </p>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {/* Product Details Tabs */}\n <Tabs defaultValue=\"description\" className=\"mt-16\">\n <TabsList className=\"grid w-full grid-cols-2\">\n <TabsTrigger value=\"description\">\n {t(\"description\", \"Description\")}\n </TabsTrigger>\n <TabsTrigger value=\"specifications\">\n {t(\"specifications\", \"Specifications\")}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"description\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"prose max-w-none\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n <Separator className=\"my-6\" />\n <h3 className=\"text-lg font-semibold mb-3\">\n {t(\"keyFeatures\", \"Key Features\")}\n </h3>\n <ul className=\"space-y-2\">\n {Array.isArray(product.tags) && product.tags.map((tag, index) => (\n <li key={index} className=\"flex items-center gap-2\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full\"></div>\n <span className=\"capitalize\">{tag}</span>\n </li>\n ))}\n </ul>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"specifications\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n {product.specifications ? (\n <div className=\"grid gap-4\">\n {Object.entries(product.specifications).map(\n ([key, value]) => (\n <div\n key={key}\n className=\"bg-muted/30 rounded-lg p-4 flex justify-between items-center\"\n >\n <span className=\"font-medium text-foreground capitalize\">\n {key\n .replace(/_/g, \" \")\n .replace(/([A-Z])/g, \" $1\")\n .trim()}\n </span>\n <span className=\"text-muted-foreground font-mono bg-background px-3 py-1 rounded-md border\">\n {value}\n </span>\n </div>\n )\n )}\n </div>\n ) : (\n <p className=\"text-muted-foreground\">\n {t(\"noSpecifications\", \"No specifications available.\")}\n </p>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n\n {/* Image Zoom Modal */}\n <Dialog\n open={isImageModalOpen}\n onOpenChange={(open) => {\n setIsImageModalOpen(open);\n if (!open) {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }\n }}\n >\n <DialogContent className=\"!max-w-[95vw] !w-[95vw] !h-[95vh] p-0 overflow-hidden bg-black/95 border-none [&>button]:hidden\">\n {/* Close Button */}\n <button\n onClick={() => setIsImageModalOpen(false)}\n className=\"absolute top-4 right-4 z-50 rounded-full bg-black/60 hover:bg-black/80 p-2 transition-all border border-white/10 backdrop-blur-md\"\n >\n <X className=\"h-6 w-6 text-white\" />\n </button>\n\n {/* Image Counter */}\n {product.images.length > 1 && (\n <div className=\"absolute top-4 left-1/2 -translate-x-1/2 z-50 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n <span className=\"text-white text-sm font-semibold\">\n {selectedImageIndex + 1} / {product.images.length}\n </span>\n </div>\n )}\n\n {/* Navigation Buttons */}\n {product.images.length > 1 && (\n <>\n <button\n className=\"absolute left-4 top-1/2 -translate-y-1/2 z-50 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md rounded-full p-3 transition-all\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronLeft className=\"h-6 w-6\" />\n </button>\n <button\n className=\"absolute right-4 top-1/2 -translate-y-1/2 z-50 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md rounded-full p-3 transition-all\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronRight className=\"h-6 w-6\" />\n </button>\n </>\n )}\n\n {/* Zoom Controls */}\n <div className=\"absolute bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n <button\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\n onClick={() => {\n const newZoom = Math.max(1, zoomLevel - 0.3);\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </button>\n <span className=\"text-white text-sm font-semibold min-w-[50px] text-center\">\n {Math.round(zoomLevel * 100)}%\n </span>\n <button\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\n onClick={() => setZoomLevel(Math.min(4, zoomLevel + 0.3))}\n disabled={zoomLevel >= 4}\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n <div className=\"w-px h-5 bg-white/20 mx-1\" />\n <button\n className=\"text-white hover:bg-white/20 text-xs px-3 h-8 rounded-full disabled:opacity-50\"\n onClick={() => {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel === 1}\n >\n Reset\n </button>\n </div>\n\n {/* Zoomable Image Container */}\n <div\n className=\"w-full h-full flex items-center justify-center overflow-hidden select-none\"\n style={{\n cursor: zoomLevel > 1 ? (isDragging ? \"grabbing\" : \"grab\") : \"zoom-in\",\n }}\n onWheel={(e) => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? -0.15 : 0.15;\n const newZoom = Math.max(1, Math.min(4, zoomLevel + delta));\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n onMouseDown={(e) => {\n if (zoomLevel > 1) {\n setIsDragging(true);\n setDragStart({\n x: e.clientX - position.x,\n y: e.clientY - position.y,\n });\n } else if (e.detail === 2) {\n setZoomLevel(2.5);\n }\n }}\n onMouseMove={(e) => {\n if (isDragging && zoomLevel > 1) {\n setPosition({\n x: e.clientX - dragStart.x,\n y: e.clientY - dragStart.y,\n });\n }\n }}\n onMouseUp={() => setIsDragging(false)}\n onMouseLeave={() => setIsDragging(false)}\n onClick={(e) => {\n if (zoomLevel === 1 && e.detail === 1) {\n setZoomLevel(2.5);\n }\n }}\n >\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"max-w-[85vw] max-h-[85vh] object-contain pointer-events-none transition-transform duration-200\"\n style={{\n transform: `scale(${zoomLevel}) translate(${position.x / zoomLevel}px, ${position.y / zoomLevel}px)`,\n }}\n draggable={false}\n />\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "product-detail-block/lang/en.json",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "product-quick-view",
|
|
3
|
+
"type": "registry:component",
|
|
4
|
+
"title": "Product Quick View",
|
|
5
|
+
"description": "Modal overlay for quick product preview with variant selection. Features image gallery, size/color selectors, and add to cart functionality.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"lucide-react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"button",
|
|
11
|
+
"dialog"
|
|
12
|
+
],
|
|
13
|
+
"usage": "import { ProductQuickView } from '@/modules/product-quick-view';\n\n<ProductQuickView product={product} open={open} onOpenChange={setOpen} />\n\n• Installed at: src/modules/product-quick-view/\n• Props: product, open, onOpenChange, onAddToCart",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "product-quick-view/product-quick-view.tsx",
|
|
17
|
+
"type": "registry:component",
|
|
18
|
+
"target": "$modules$/product-quick-view/product-quick-view.tsx",
|
|
19
|
+
"content": "\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Link } from \"react-router\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogTitle,\r\n} from \"@/components/ui/dialog\";\r\nimport { X, Minus, Plus, ShoppingCart } from \"lucide-react\";\r\n\r\ninterface ProductVariant {\r\n id: string;\r\n name: string;\r\n available?: boolean;\r\n}\r\n\r\ninterface Product {\r\n id: string;\r\n title: string;\r\n price: number;\r\n originalPrice?: number;\r\n description: string;\r\n images: string[];\r\n sizes?: ProductVariant[];\r\n colors?: ProductVariant[];\r\n link?: string;\r\n}\r\n\r\ninterface ProductQuickViewProps {\r\n product: Product;\r\n open: boolean;\r\n onOpenChange: (open: boolean) => void;\r\n onAddToCart?: (product: Product, quantity: number, selectedSize?: string, selectedColor?: string) => void;\r\n className?: string;\r\n}\r\n\r\nexport function ProductQuickView({\r\n product,\r\n open,\r\n onOpenChange,\r\n onAddToCart,\r\n className,\r\n}: ProductQuickViewProps) {\r\n const { t } = useTranslation(\"product-quick-view\");\r\n const [selectedImage, setSelectedImage] = useState(0);\r\n const [quantity, setQuantity] = useState(1);\r\n const [selectedSize, setSelectedSize] = useState<string | undefined>(\r\n product.sizes?.[0]?.id\r\n );\r\n const [selectedColor, setSelectedColor] = useState<string | undefined>(\r\n product.colors?.[0]?.id\r\n );\r\n\r\n const formatPrice = (price: number) => {\r\n return new Intl.NumberFormat(\"en-US\", {\r\n style: \"currency\",\r\n currency: \"USD\",\r\n }).format(price);\r\n };\r\n\r\n const handleAddToCart = () => {\r\n onAddToCart?.(product, quantity, selectedSize, selectedColor);\r\n onOpenChange(false);\r\n };\r\n\r\n const incrementQuantity = () => setQuantity((q) => q + 1);\r\n const decrementQuantity = () => setQuantity((q) => Math.max(1, q - 1));\r\n\r\n const discount = product.originalPrice\r\n ? Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)\r\n : 0;\r\n\r\n return (\r\n <Dialog open={open} onOpenChange={onOpenChange}>\r\n <DialogContent className={cn(\"sm:max-w-4xl p-0 gap-0 overflow-hidden\", className)}>\r\n <DialogTitle className=\"sr-only\">{product.title}</DialogTitle>\r\n\r\n {/* Close button */}\r\n <button\r\n onClick={() => onOpenChange(false)}\r\n className=\"absolute right-4 top-4 z-10 rounded-full bg-background/80 backdrop-blur-sm p-2 hover:bg-background transition-colors\"\r\n >\r\n <X className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Close</span>\r\n </button>\r\n\r\n <div className=\"grid md:grid-cols-2\">\r\n {/* Image Gallery */}\r\n <div className=\"relative bg-muted aspect-square md:aspect-auto md:h-full\">\r\n {/* Main Image */}\r\n <img\r\n src={product.images[selectedImage]}\r\n alt={product.title}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n\r\n {/* Discount Badge */}\r\n {discount > 0 && (\r\n <span className=\"absolute top-4 left-4 bg-destructive text-destructive-foreground text-sm font-semibold px-3 py-1 rounded-full\">\r\n -{discount}%\r\n </span>\r\n )}\r\n\r\n {/* Thumbnails */}\r\n {product.images.length > 1 && (\r\n <div className=\"absolute bottom-4 left-4 flex flex-col gap-2\">\r\n {product.images.map((image, index) => (\r\n <button\r\n key={index}\r\n onClick={() => setSelectedImage(index)}\r\n className={cn(\r\n \"w-14 h-14 rounded-lg overflow-hidden border-2 transition-all bg-background/80 backdrop-blur-sm\",\r\n selectedImage === index\r\n ? \"border-primary ring-2 ring-primary/20\"\r\n : \"border-transparent opacity-70 hover:opacity-100\"\r\n )}\r\n >\r\n <img\r\n src={image}\r\n alt={`${product.title} ${index + 1}`}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n </button>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Product Info */}\r\n <div className=\"p-6 md:p-8 flex flex-col\">\r\n <div className=\"flex-1\">\r\n <h2 className=\"text-2xl md:text-3xl font-bold mb-2\">\r\n {product.title}\r\n </h2>\r\n\r\n {/* Price */}\r\n <div className=\"flex items-center gap-3 mb-4\">\r\n <span className=\"text-2xl font-bold text-primary\">\r\n {formatPrice(product.price)}\r\n </span>\r\n {product.originalPrice && (\r\n <span className=\"text-lg text-muted-foreground line-through\">\r\n {formatPrice(product.originalPrice)}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {/* Description */}\r\n <p className=\"text-muted-foreground mb-6\">\r\n {product.description}\r\n </p>\r\n\r\n {/* Size Selector */}\r\n {product.sizes && product.sizes.length > 0 && (\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"size\")}\r\n </label>\r\n <div className=\"flex flex-wrap gap-2\">\r\n {product.sizes.map((size) => (\r\n <button\r\n key={size.id}\r\n onClick={() => setSelectedSize(size.id)}\r\n disabled={size.available === false}\r\n className={cn(\r\n \"px-4 py-2 rounded-lg border text-sm font-medium transition-all\",\r\n selectedSize === size.id\r\n ? \"border-primary bg-primary text-primary-foreground\"\r\n : \"border-border hover:border-primary\",\r\n size.available === false && \"opacity-50 cursor-not-allowed line-through\"\r\n )}\r\n >\r\n {size.name}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Color Selector */}\r\n {product.colors && product.colors.length > 0 && (\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"color\")}\r\n </label>\r\n <div className=\"flex flex-wrap gap-2\">\r\n {product.colors.map((color) => (\r\n <button\r\n key={color.id}\r\n onClick={() => setSelectedColor(color.id)}\r\n disabled={color.available === false}\r\n className={cn(\r\n \"px-4 py-2 rounded-lg border text-sm font-medium transition-all\",\r\n selectedColor === color.id\r\n ? \"border-primary bg-primary text-primary-foreground\"\r\n : \"border-border hover:border-primary\",\r\n color.available === false && \"opacity-50 cursor-not-allowed line-through\"\r\n )}\r\n >\r\n {color.name}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Quantity */}\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"quantity\")}\r\n </label>\r\n <div className=\"flex items-center gap-3\">\r\n <div className=\"flex items-center border border-border rounded-lg\">\r\n <button\r\n onClick={decrementQuantity}\r\n className=\"p-3 hover:bg-muted transition-colors\"\r\n disabled={quantity <= 1}\r\n >\r\n <Minus className=\"h-4 w-4\" />\r\n </button>\r\n <span className=\"w-12 text-center font-medium\">\r\n {quantity}\r\n </span>\r\n <button\r\n onClick={incrementQuantity}\r\n className=\"p-3 hover:bg-muted transition-colors\"\r\n >\r\n <Plus className=\"h-4 w-4\" />\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"space-y-3 pt-4 border-t border-border\">\r\n <Button\r\n onClick={handleAddToCart}\r\n className=\"w-full gap-2\"\r\n size=\"lg\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {t(\"addToCart\")}\r\n </Button>\r\n\r\n {product.link && (\r\n <Link to={product.link} className=\"block\">\r\n <Button variant=\"outline\" className=\"w-full\" size=\"lg\">\r\n {t(\"viewDetails\")}\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n );\r\n}\r\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "product-quick-view/index.ts",
|
|
23
|
+
"type": "registry:index",
|
|
24
|
+
"target": "$modules$/product-quick-view/index.ts",
|
|
25
|
+
"content": "export * from \"./product-quick-view\";\r\n"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "product-quick-view/lang/en.json",
|
|
29
|
+
"type": "registry:lang",
|
|
30
|
+
"target": "$modules$/product-quick-view/lang/en.json",
|
|
31
|
+
"content": "{\r\n \"size\": \"Size\",\r\n \"color\": \"Color\",\r\n \"quantity\": \"Quantity\",\r\n \"addToCart\": \"Add to Cart\",\r\n \"viewDetails\": \"View Full Details\"\r\n}\r\n"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "product-quick-view/lang/tr.json",
|
|
35
|
+
"type": "registry:lang",
|
|
36
|
+
"target": "$modules$/product-quick-view/lang/tr.json",
|
|
37
|
+
"content": "{\r\n \"size\": \"Beden\",\r\n \"color\": \"Renk\",\r\n \"quantity\": \"Adet\",\r\n \"addToCart\": \"Sepete Ekle\",\r\n \"viewDetails\": \"Tüm Detayları Gör\"\r\n}\r\n"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"exports": {
|
|
41
|
+
"types": [],
|
|
42
|
+
"variables": [
|
|
43
|
+
"ProductQuickView"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|