@promakeai/cli 0.4.6 → 0.5.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/README.md +71 -0
- package/dist/index.js +149 -156
- package/dist/registry/blog-core.json +7 -26
- package/dist/registry/blog-list-page.json +2 -2
- package/dist/registry/blog-section.json +1 -1
- package/dist/registry/cart-drawer.json +1 -1
- package/dist/registry/cart-page.json +1 -1
- package/dist/registry/category-section.json +1 -1
- package/dist/registry/checkout-page.json +1 -1
- package/dist/registry/docs/blog-core.md +12 -13
- package/dist/registry/docs/blog-list-page.md +1 -1
- package/dist/registry/docs/ecommerce-core.md +10 -13
- package/dist/registry/docs/featured-products.md +1 -1
- package/dist/registry/docs/post-detail-page.md +2 -2
- package/dist/registry/docs/product-detail-page.md +2 -2
- package/dist/registry/docs/products-page.md +1 -1
- package/dist/registry/ecommerce-core.json +5 -25
- package/dist/registry/featured-products.json +2 -2
- package/dist/registry/header-centered-pill.json +1 -1
- package/dist/registry/header-ecommerce.json +1 -1
- package/dist/registry/index.json +0 -1
- package/dist/registry/login-page.json +1 -1
- package/dist/registry/post-card.json +1 -1
- package/dist/registry/post-detail-block.json +1 -1
- package/dist/registry/post-detail-page.json +3 -3
- package/dist/registry/product-card-detailed.json +1 -1
- package/dist/registry/product-card.json +1 -1
- package/dist/registry/product-detail-block.json +1 -1
- package/dist/registry/product-detail-page.json +3 -3
- package/dist/registry/product-detail-section.json +1 -1
- package/dist/registry/product-quick-view.json +1 -1
- package/dist/registry/products-page.json +2 -2
- package/dist/registry/register-page.json +1 -1
- package/dist/registry/related-products-block.json +1 -1
- package/package.json +4 -2
- package/template/README.md +54 -73
- package/template/package.json +4 -3
- package/template/public/data/database.db +0 -0
- package/template/public/data/database.db-shm +0 -0
- package/template/public/data/database.db-wal +0 -0
- package/template/scripts/init-db.ts +13 -126
- package/template/src/App.tsx +8 -5
- package/template/src/db/index.ts +20 -0
- package/template/src/db/provider.tsx +77 -0
- package/template/src/db/schema.json +259 -0
- package/template/src/db/types.ts +195 -0
- package/template/src/hooks/use-debounced-value.ts +12 -0
- package/dist/registry/db.json +0 -129
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"path": "/products",
|
|
13
13
|
"componentName": "ProductsPage"
|
|
14
14
|
},
|
|
15
|
-
"usage": "import ProductsPage from '@/modules/products-page';\n\n<Route path=\"/products\" element={<ProductsPage />} />\n\n• Installed at: src/modules/products-page/\n• Add link: <Link to=\"/products\">Browse Products</Link>\n• Supports filters, sorting, grid/list view, pagination\n• Uses
|
|
15
|
+
"usage": "import ProductsPage from '@/modules/products-page';\n\n<Route path=\"/products\" element={<ProductsPage />} />\n\n• Installed at: src/modules/products-page/\n• Add link: <Link to=\"/products\">Browse Products</Link>\n• Supports filters, sorting, grid/list view, pagination\n• Uses useDbList from @/db for data fetching",
|
|
16
16
|
"files": [
|
|
17
17
|
{
|
|
18
18
|
"path": "products-page/index.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"path": "products-page/products-page.tsx",
|
|
25
25
|
"type": "registry:page",
|
|
26
26
|
"target": "$modules$/products-page/products-page.tsx",
|
|
27
|
-
"content": "import { useState, useRef, useCallback, useMemo } from \"react\";\r\nimport { useSearchParams } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Filter, Grid, List } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetDescription,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useDbProducts, useDbCategories } from \"@/modules/ecommerce-core\";\r\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface FilterSidebarProps {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n t: any;\r\n categories: Category[];\r\n selectedCategories: string[];\r\n handleCategoryChange: (category: string, checked: boolean) => void;\r\n selectedFeatures: string[];\r\n handleFeatureChange: (feature: string, checked: boolean) => void;\r\n minPriceRef: React.RefObject<HTMLInputElement | null>;\r\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\r\n searchParams: URLSearchParams;\r\n handlePriceFilter: () => void;\r\n}\r\n\r\nfunction FilterSidebar({\r\n t,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n selectedFeatures,\r\n handleFeatureChange,\r\n minPriceRef,\r\n maxPriceRef,\r\n searchParams,\r\n handlePriceFilter,\r\n}: FilterSidebarProps) {\r\n return (\r\n <div className=\"space-y-6\">\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"categories\", \"Categories\")}\r\n </h3>\r\n <div className=\"space-y-3\">\r\n {categories.map((category) => (\r\n <div\r\n key={category.id}\r\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\r\n data-db-table=\"product_categories\"\r\n data-db-id={category.id}\r\n >\r\n <Checkbox\r\n id={`category-${category.id}`}\r\n checked={selectedCategories.includes(category.slug)}\r\n onCheckedChange={(checked) =>\r\n handleCategoryChange(category.slug, checked as boolean)\r\n }\r\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\r\n />\r\n <label\r\n htmlFor={`category-${category.id}`}\r\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\r\n >\r\n {category.name}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"priceRange\", \"Price Range\")}\r\n </h3>\r\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\r\n <div className=\"grid grid-cols-2 gap-3\">\r\n <input\r\n ref={minPriceRef}\r\n type=\"number\"\r\n placeholder={t(\"minPrice\", \"Min\")}\r\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\r\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\r\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\r\n />\r\n <input\r\n ref={maxPriceRef}\r\n type=\"number\"\r\n placeholder={t(\"maxPrice\", \"Max\")}\r\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\r\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\r\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"features\", \"Features\")}\r\n </h3>\r\n <div className=\"space-y-3\">\r\n {[\r\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\r\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\r\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\r\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\r\n ].map((feature) => (\r\n <div\r\n key={feature.key}\r\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\r\n >\r\n <Checkbox\r\n id={feature.key}\r\n checked={selectedFeatures.includes(feature.key)}\r\n onCheckedChange={(checked) =>\r\n handleFeatureChange(feature.key, checked as boolean)\r\n }\r\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\r\n />\r\n <label\r\n htmlFor={feature.key}\r\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\r\n >\r\n {feature.label}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport function ProductsPage() {\r\n const { t } = useTranslation(\"products-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\r\n const { products, loading: productsLoading } = useDbProducts();\r\n const { categories, loading: categoriesLoading } = useDbCategories();\r\n const loading = productsLoading || categoriesLoading;\r\n\r\n const [searchParams, setSearchParams] = useSearchParams();\r\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\r\n const [sortBy, setSortBy] = useState(\"featured\");\r\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\r\n const categorySlug = searchParams.get(\"category\");\r\n return categorySlug ? [categorySlug] : [];\r\n });\r\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\r\n const searchQuery = searchParams.get(\"search\") || \"\";\r\n const minPriceRef = useRef<HTMLInputElement>(null);\r\n const maxPriceRef = useRef<HTMLInputElement>(null);\r\n\r\n const filteredProducts = useMemo(() => {\r\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\r\n const maxPrice =\r\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\r\n\r\n let filtered = products.filter((product) => {\r\n const currentPrice =\r\n product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n return currentPrice >= minPrice && currentPrice <= maxPrice;\r\n });\r\n\r\n if (selectedCategories.length > 0) {\r\n filtered = filtered.filter((product) => {\r\n return selectedCategories.some((selectedCategory) => {\r\n if (product.category === selectedCategory) return true;\r\n return product.categories?.some(\r\n (cat) => cat.slug === selectedCategory\r\n );\r\n });\r\n });\r\n }\r\n\r\n if (selectedFeatures.length > 0) {\r\n filtered = filtered.filter((product) => {\r\n return selectedFeatures.every((feature) => {\r\n switch (feature) {\r\n case \"on_sale\":\r\n return product.on_sale;\r\n case \"is_new\":\r\n return product.is_new;\r\n case \"featured\":\r\n return product.featured;\r\n case \"in_stock\":\r\n return product.stock > 0;\r\n default:\r\n return true;\r\n }\r\n });\r\n });\r\n }\r\n\r\n // Apply sorting\r\n return [...filtered].sort((a, b) => {\r\n switch (sortBy) {\r\n case \"price-low\":\r\n return (\r\n (a.on_sale ? a.sale_price || a.price : a.price) -\r\n (b.on_sale ? b.sale_price || b.price : b.price)\r\n );\r\n case \"price-high\":\r\n return (\r\n (b.on_sale ? b.sale_price || b.price : b.price) -\r\n (a.on_sale ? a.sale_price || a.price : a.price)\r\n );\r\n case \"newest\":\r\n return (\r\n new Date(b.created_at || 0).getTime() -\r\n new Date(a.created_at || 0).getTime()\r\n );\r\n case \"featured\":\r\n default:\r\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\r\n }\r\n });\r\n }, [products, searchParams, selectedFeatures, selectedCategories, sortBy]);\r\n\r\n const handlePriceFilter = useCallback(() => {\r\n const minPrice = minPriceRef.current?.value || \"\";\r\n const maxPrice = maxPriceRef.current?.value || \"\";\r\n const params = new URLSearchParams(searchParams);\r\n if (minPrice) params.set(\"minPrice\", minPrice);\r\n else params.delete(\"minPrice\");\r\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\r\n else params.delete(\"maxPrice\");\r\n setSearchParams(params);\r\n }, [searchParams, setSearchParams]);\r\n\r\n const handleCategoryChange = useCallback(\r\n (category: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedCategories((prev) => [...prev, category]);\r\n } else {\r\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\r\n }\r\n },\r\n []\r\n );\r\n\r\n const handleFeatureChange = useCallback(\r\n (feature: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedFeatures((prev) => [...prev, feature]);\r\n } else {\r\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\r\n }\r\n },\r\n []\r\n );\r\n\r\n const sortOptions = [\r\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\r\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\r\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\r\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\r\n ];\r\n\r\n const filterSidebarProps: FilterSidebarProps = {\r\n t,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n selectedFeatures,\r\n handleFeatureChange,\r\n minPriceRef,\r\n maxPriceRef,\r\n searchParams,\r\n handlePriceFilter,\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"mb-8\">\r\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\r\n <div className=\"space-y-1\">\r\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\r\n {searchQuery\r\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\r\n : t(\"allProducts\", \"All Products\")}\r\n </h1>\r\n <p className=\"text-sm lg:text-base text-muted-foreground\">\r\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\r\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\r\n </p>\r\n </div>\r\n {searchQuery && (\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={() => setSearchParams({})}\r\n className=\"w-fit\"\r\n >\r\n {t(\"clearSearch\", \"Clear Search\")}\r\n </Button>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\r\n <Sheet>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"outline\"\r\n className=\"lg:hidden w-full sm:w-auto\"\r\n >\r\n <Filter className=\"h-4 w-4 mr-2\" />\r\n {t(\"filters\", \"Filters\")}\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"left\" className=\"w-[300px]\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\r\n <SheetDescription>\r\n {t(\"refineSearch\", \"Refine your product search\")}\r\n </SheetDescription>\r\n </SheetHeader>\r\n <div className=\"mt-6\">\r\n <FilterSidebar {...filterSidebarProps} />\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n\r\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\r\n <Select value={sortBy} onValueChange={setSortBy}>\r\n <SelectTrigger className=\"w-full sm:w-[160px]\">\r\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {sortOptions.map((option) => (\r\n <SelectItem key={option.value} value={option.value}>\r\n {option.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n\r\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\r\n <Button\r\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\r\n size=\"sm\"\r\n onClick={() => setViewMode(\"grid\")}\r\n className=\"flex-1 sm:flex-none\"\r\n >\r\n <Grid className=\"h-4 w-4\" />\r\n </Button>\r\n <Button\r\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\r\n size=\"sm\"\r\n onClick={() => setViewMode(\"list\")}\r\n className=\"flex-1 sm:flex-none\"\r\n >\r\n <List className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"flex gap-8\">\r\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\r\n <div className=\"sticky top-24\">\r\n <FilterSidebar {...filterSidebarProps} />\r\n </div>\r\n </aside>\r\n\r\n <div className=\"flex-1\">\r\n {loading ? (\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\r\n {[...Array(6)].map((_, i) => (\r\n <div\r\n key={i}\r\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\r\n >\r\n <div className=\"aspect-square bg-muted mb-4\"></div>\r\n <div className=\"p-4\">\r\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\r\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n ) : viewMode === \"grid\" ? (\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\r\n {filteredProducts.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"grid\"\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <div className=\"space-y-6\">\r\n {filteredProducts.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"list\"\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n\r\n {!loading && filteredProducts.length === 0 && (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"noProductsFound\",\r\n \"No products found matching your criteria.\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ProductsPage;\r\n"
|
|
27
|
+
"content": "import { useState, useRef, useCallback, useMemo } from \"react\";\r\nimport { useSearchParams } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Filter, Grid, List } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetDescription,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useDbList } from \"@/db\";\r\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface FilterSidebarProps {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n t: any;\r\n categories: Category[];\r\n selectedCategories: string[];\r\n handleCategoryChange: (category: string, checked: boolean) => void;\r\n selectedFeatures: string[];\r\n handleFeatureChange: (feature: string, checked: boolean) => void;\r\n minPriceRef: React.RefObject<HTMLInputElement | null>;\r\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\r\n searchParams: URLSearchParams;\r\n handlePriceFilter: () => void;\r\n}\r\n\r\nfunction FilterSidebar({\r\n t,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n selectedFeatures,\r\n handleFeatureChange,\r\n minPriceRef,\r\n maxPriceRef,\r\n searchParams,\r\n handlePriceFilter,\r\n}: FilterSidebarProps) {\r\n return (\r\n <div className=\"space-y-6\">\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"categories\", \"Categories\")}\r\n </h3>\r\n <div className=\"space-y-3\">\r\n {categories.map((category) => (\r\n <div\r\n key={category.id}\r\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\r\n data-db-table=\"product_categories\"\r\n data-db-id={category.id}\r\n >\r\n <Checkbox\r\n id={`category-${category.id}`}\r\n checked={selectedCategories.includes(category.slug)}\r\n onCheckedChange={(checked) =>\r\n handleCategoryChange(category.slug, checked as boolean)\r\n }\r\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\r\n />\r\n <label\r\n htmlFor={`category-${category.id}`}\r\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\r\n >\r\n {category.name}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"priceRange\", \"Price Range\")}\r\n </h3>\r\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\r\n <div className=\"grid grid-cols-2 gap-3\">\r\n <input\r\n ref={minPriceRef}\r\n type=\"number\"\r\n placeholder={t(\"minPrice\", \"Min\")}\r\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\r\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\r\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\r\n />\r\n <input\r\n ref={maxPriceRef}\r\n type=\"number\"\r\n placeholder={t(\"maxPrice\", \"Max\")}\r\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\r\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\r\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"features\", \"Features\")}\r\n </h3>\r\n <div className=\"space-y-3\">\r\n {[\r\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\r\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\r\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\r\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\r\n ].map((feature) => (\r\n <div\r\n key={feature.key}\r\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\r\n >\r\n <Checkbox\r\n id={feature.key}\r\n checked={selectedFeatures.includes(feature.key)}\r\n onCheckedChange={(checked) =>\r\n handleFeatureChange(feature.key, checked as boolean)\r\n }\r\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\r\n />\r\n <label\r\n htmlFor={feature.key}\r\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\r\n >\r\n {feature.label}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport function ProductsPage() {\r\n const { t } = useTranslation(\"products-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\r\n const { data: products = [], isLoading: productsLoading } = useDbList<Product>(\"products\", {\r\n where: { published: 1 },\r\n });\r\n const { data: categories = [], isLoading: categoriesLoading } = useDbList<Category>(\"product_categories\");\r\n const loading = productsLoading || categoriesLoading;\r\n\r\n const [searchParams, setSearchParams] = useSearchParams();\r\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\r\n const [sortBy, setSortBy] = useState(\"featured\");\r\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\r\n const categorySlug = searchParams.get(\"category\");\r\n return categorySlug ? [categorySlug] : [];\r\n });\r\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\r\n const searchQuery = searchParams.get(\"search\") || \"\";\r\n const minPriceRef = useRef<HTMLInputElement>(null);\r\n const maxPriceRef = useRef<HTMLInputElement>(null);\r\n\r\n const selectedCategoryIds = useMemo(() => {\r\n if (selectedCategories.length === 0) return new Set<number>();\r\n return new Set(\r\n categories.filter(c => selectedCategories.includes(c.slug)).map(c => c.id)\r\n );\r\n }, [selectedCategories, categories]);\r\n\r\n const filteredProducts = useMemo(() => {\r\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\r\n const maxPrice =\r\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\r\n\r\n let filtered = products.filter((product) => {\r\n const currentPrice =\r\n product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n return currentPrice >= minPrice && currentPrice <= maxPrice;\r\n });\r\n\r\n if (selectedCategories.length > 0) {\r\n filtered = filtered.filter((product) =>\r\n product.categories?.some((id) => selectedCategoryIds.has(id))\r\n );\r\n }\r\n\r\n if (selectedFeatures.length > 0) {\r\n filtered = filtered.filter((product) => {\r\n return selectedFeatures.every((feature) => {\r\n switch (feature) {\r\n case \"on_sale\":\r\n return product.on_sale;\r\n case \"is_new\":\r\n return product.is_new;\r\n case \"featured\":\r\n return product.featured;\r\n case \"in_stock\":\r\n return product.stock > 0;\r\n default:\r\n return true;\r\n }\r\n });\r\n });\r\n }\r\n\r\n // Apply sorting\r\n return [...filtered].sort((a, b) => {\r\n switch (sortBy) {\r\n case \"price-low\":\r\n return (\r\n (a.on_sale ? a.sale_price || a.price : a.price) -\r\n (b.on_sale ? b.sale_price || b.price : b.price)\r\n );\r\n case \"price-high\":\r\n return (\r\n (b.on_sale ? b.sale_price || b.price : b.price) -\r\n (a.on_sale ? a.sale_price || a.price : a.price)\r\n );\r\n case \"newest\":\r\n return (\r\n new Date(b.created_at || 0).getTime() -\r\n new Date(a.created_at || 0).getTime()\r\n );\r\n case \"featured\":\r\n default:\r\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\r\n }\r\n });\r\n }, [products, searchParams, selectedFeatures, selectedCategories, selectedCategoryIds, sortBy]);\r\n\r\n const handlePriceFilter = useCallback(() => {\r\n const minPrice = minPriceRef.current?.value || \"\";\r\n const maxPrice = maxPriceRef.current?.value || \"\";\r\n const params = new URLSearchParams(searchParams);\r\n if (minPrice) params.set(\"minPrice\", minPrice);\r\n else params.delete(\"minPrice\");\r\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\r\n else params.delete(\"maxPrice\");\r\n setSearchParams(params);\r\n }, [searchParams, setSearchParams]);\r\n\r\n const handleCategoryChange = useCallback(\r\n (category: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedCategories((prev) => [...prev, category]);\r\n } else {\r\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\r\n }\r\n },\r\n []\r\n );\r\n\r\n const handleFeatureChange = useCallback(\r\n (feature: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedFeatures((prev) => [...prev, feature]);\r\n } else {\r\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\r\n }\r\n },\r\n []\r\n );\r\n\r\n const sortOptions = [\r\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\r\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\r\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\r\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\r\n ];\r\n\r\n const filterSidebarProps: FilterSidebarProps = {\r\n t,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n selectedFeatures,\r\n handleFeatureChange,\r\n minPriceRef,\r\n maxPriceRef,\r\n searchParams,\r\n handlePriceFilter,\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"mb-8\">\r\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\r\n <div className=\"space-y-1\">\r\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\r\n {searchQuery\r\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\r\n : t(\"allProducts\", \"All Products\")}\r\n </h1>\r\n <p className=\"text-sm lg:text-base text-muted-foreground\">\r\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\r\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\r\n </p>\r\n </div>\r\n {searchQuery && (\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={() => setSearchParams({})}\r\n className=\"w-fit\"\r\n >\r\n {t(\"clearSearch\", \"Clear Search\")}\r\n </Button>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\r\n <Sheet>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"outline\"\r\n className=\"lg:hidden w-full sm:w-auto\"\r\n >\r\n <Filter className=\"h-4 w-4 mr-2\" />\r\n {t(\"filters\", \"Filters\")}\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"left\" className=\"w-[300px]\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\r\n <SheetDescription>\r\n {t(\"refineSearch\", \"Refine your product search\")}\r\n </SheetDescription>\r\n </SheetHeader>\r\n <div className=\"mt-6\">\r\n <FilterSidebar {...filterSidebarProps} />\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n\r\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\r\n <Select value={sortBy} onValueChange={setSortBy}>\r\n <SelectTrigger className=\"w-full sm:w-[160px]\">\r\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {sortOptions.map((option) => (\r\n <SelectItem key={option.value} value={option.value}>\r\n {option.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n\r\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\r\n <Button\r\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\r\n size=\"sm\"\r\n onClick={() => setViewMode(\"grid\")}\r\n className=\"flex-1 sm:flex-none\"\r\n >\r\n <Grid className=\"h-4 w-4\" />\r\n </Button>\r\n <Button\r\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\r\n size=\"sm\"\r\n onClick={() => setViewMode(\"list\")}\r\n className=\"flex-1 sm:flex-none\"\r\n >\r\n <List className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"flex gap-8\">\r\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\r\n <div className=\"sticky top-24\">\r\n <FilterSidebar {...filterSidebarProps} />\r\n </div>\r\n </aside>\r\n\r\n <div className=\"flex-1\">\r\n {loading ? (\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\r\n {[...Array(6)].map((_, i) => (\r\n <div\r\n key={i}\r\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\r\n >\r\n <div className=\"aspect-square bg-muted mb-4\"></div>\r\n <div className=\"p-4\">\r\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\r\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n ) : viewMode === \"grid\" ? (\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\r\n {filteredProducts.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"grid\"\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <div className=\"space-y-6\">\r\n {filteredProducts.map((product) => (\r\n <div\r\n key={product.id}\r\n className=\"w-full\"\r\n data-db-table=\"products\"\r\n data-db-id={product.id}\r\n >\r\n <ProductCard\r\n product={product}\r\n variant=\"list\"\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n\r\n {!loading && filteredProducts.length === 0 && (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"noProductsFound\",\r\n \"No products found matching your criteria.\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ProductsPage;\r\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "products-page/lang/en.json",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"path": "register-page/register-page.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/register-page/register-page.tsx",
|
|
26
|
-
"content": "import { useState, useEffect } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { UserPlus, Eye, EyeOff, CheckCircle } from \"lucide-react\";\r\n\r\nexport function RegisterPage() {\r\n const { t } = useTranslation(\"register-page\");\r\n usePageTitle({ title: t(\"title\", \"Create Account\") });\r\n\r\n const navigate = useNavigate();\r\n const { register, isAuthenticated } = useAuth();\r\n\r\n const [formData, setFormData] = useState({\r\n username: \"\",\r\n email: \"\",\r\n password: \"\",\r\n confirmPassword: \"\",\r\n });\r\n const [showPassword, setShowPassword] = useState(false);\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [success, setSuccess] = useState(false);\r\n\r\n // Redirect if already authenticated\r\n useEffect(() => {\r\n if (isAuthenticated) {\r\n navigate(\"/\", { replace: true });\r\n }\r\n }, [isAuthenticated, navigate]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (formData.password !== formData.confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\r\n description: t(\"passwordMismatch\", \"Passwords do not match\"),\r\n });\r\n setIsSubmitting(false);\r\n return;\r\n }\r\n\r\n try {\r\n await register(formData.username, formData.email, formData.password);\r\n setSuccess(true);\r\n toast.success(t(\"toastSuccessTitle\", \"Account created!\"), {\r\n description: t(\r\n \"toastSuccessDesc\",\r\n \"Please check your email to verify your account.\",\r\n ),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\", \"Registration failed. Please try again.\"));\r\n setError(errorMessage);\r\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n if (success) {\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <div className=\"text-center space-y-4\">\r\n <CheckCircle className=\"w-16 h-16 text-green-500 mx-auto\" />\r\n <h2 className=\"text-2xl font-bold text-foreground\">\r\n {t(\"successTitle\", \"Account Created!\")}\r\n </h2>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"successMessage\", \"Please check your email to verify your account.\")}\r\n </p>\r\n <Button asChild className=\"mt-4\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Create Account\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {t(\"description\", \"Create an account to get started\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <UserPlus className=\"w-5 h-5 text-primary\" />\r\n {t(\"cardTitle\", \"Sign Up\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n <div>\r\n <Label htmlFor=\"username\">{t(\"username\", \"Username\")} *</Label>\r\n <Input\r\n id=\"username\"\r\n name=\"username\"\r\n type=\"text\"\r\n value={formData.username}\r\n onChange={handleChange}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"username\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"email\", \"Email\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"email\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"password\">{t(\"password\", \"Password\")} *</Label>\r\n <div className=\"relative\">\r\n <Input\r\n id=\"password\"\r\n name=\"password\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={formData.password}\r\n onChange={handleChange}\r\n placeholder={t(\"passwordPlaceholder\", \"Enter password\")}\r\n required\r\n className=\"mt-1 pr-10\"\r\n autoComplete=\"new-password\"\r\n />\r\n <button\r\n type=\"button\"\r\n onClick={() => setShowPassword(!showPassword)}\r\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\r\n >\r\n {showPassword ? (\r\n <EyeOff className=\"w-4 h-4\" />\r\n ) : (\r\n <Eye className=\"w-4 h-4\" />\r\n )}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"confirmPassword\">\r\n {t(\"confirmPassword\", \"Confirm Password\")} *\r\n </Label>\r\n <Input\r\n id=\"confirmPassword\"\r\n name=\"confirmPassword\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={formData.confirmPassword}\r\n onChange={handleChange}\r\n placeholder={t(\"confirmPasswordPlaceholder\", \"Confirm your password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"submitting\", \"Creating account...\")}\r\n </>\r\n ) : (\r\n t(\"submit\", \"Create Account\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"hasAccount\", \"Already have an account?\")}{\" \"}\r\n <Link\r\n to=\"/login\"\r\n className=\"text-primary hover:underline font-medium\"\r\n >\r\n {t(\"loginLink\", \"Sign in\")}\r\n </Link>\r\n </div>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default RegisterPage;\r\n"
|
|
26
|
+
"content": "import { useState, useEffect } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { UserPlus, Eye, EyeOff, CheckCircle } from \"lucide-react\";\r\n\r\nexport function RegisterPage() {\r\n const { t } = useTranslation(\"register-page\");\r\n usePageTitle({ title: t(\"title\", \"Create Account\") });\r\n\r\n const navigate = useNavigate();\r\n const { register, isAuthenticated } = useAuth();\r\n\r\n const [formData, setFormData] = useState({\r\n username: \"\",\r\n email: \"\",\r\n password: \"\",\r\n confirmPassword: \"\",\r\n });\r\n const [showPassword, setShowPassword] = useState(false);\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [success, setSuccess] = useState(false);\r\n\r\n // Redirect if already authenticated\r\n useEffect(() => {\r\n if (isAuthenticated) {\r\n navigate(\"/\", { replace: true });\r\n }\r\n }, [isAuthenticated, navigate]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (formData.password !== formData.confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\r\n description: t(\"passwordMismatch\", \"Passwords do not match\"),\r\n });\r\n setIsSubmitting(false);\r\n return;\r\n }\r\n\r\n try {\r\n await register(formData.username, formData.email, formData.password);\r\n setSuccess(true);\r\n toast.success(t(\"toastSuccessTitle\", \"Account created!\"), {\r\n description: t(\r\n \"toastSuccessDesc\",\r\n \"Please check your email to verify your account.\",\r\n ),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\", \"Registration failed. Please try again.\"));\r\n setError(errorMessage);\r\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n if (success) {\r\n return (\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <div className=\"text-center space-y-4\">\r\n <CheckCircle className=\"w-16 h-16 text-green-500 mx-auto\" />\r\n <h2 className=\"text-2xl font-bold text-foreground\">\r\n {t(\"successTitle\", \"Account Created!\")}\r\n </h2>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"successMessage\", \"Please check your email to verify your account.\")}\r\n </p>\r\n <Button asChild className=\"mt-4\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Create Account\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {t(\"description\", \"Create an account to get started\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <UserPlus className=\"w-5 h-5 text-primary\" />\r\n {t(\"cardTitle\", \"Sign Up\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n <div>\r\n <Label htmlFor=\"username\">{t(\"username\", \"Username\")} *</Label>\r\n <Input\r\n id=\"username\"\r\n name=\"username\"\r\n type=\"text\"\r\n value={formData.username}\r\n onChange={handleChange}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"username\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"email\", \"Email\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"email\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"password\">{t(\"password\", \"Password\")} *</Label>\r\n <div className=\"relative\">\r\n <Input\r\n id=\"password\"\r\n name=\"password\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={formData.password}\r\n onChange={handleChange}\r\n placeholder={t(\"passwordPlaceholder\", \"Enter password\")}\r\n required\r\n className=\"mt-1 pr-10\"\r\n autoComplete=\"new-password\"\r\n />\r\n <button\r\n type=\"button\"\r\n onClick={() => setShowPassword(!showPassword)}\r\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\r\n >\r\n {showPassword ? (\r\n <EyeOff className=\"w-4 h-4\" />\r\n ) : (\r\n <Eye className=\"w-4 h-4\" />\r\n )}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"confirmPassword\">\r\n {t(\"confirmPassword\", \"Confirm Password\")} *\r\n </Label>\r\n <Input\r\n id=\"confirmPassword\"\r\n name=\"confirmPassword\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={formData.confirmPassword}\r\n onChange={handleChange}\r\n placeholder={t(\"confirmPasswordPlaceholder\", \"Confirm your password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"submitting\", \"Creating account...\")}\r\n </>\r\n ) : (\r\n t(\"submit\", \"Create Account\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"hasAccount\", \"Already have an account?\")}{\" \"}\r\n <Link\r\n to=\"/login\"\r\n className=\"text-primary hover:underline font-medium\"\r\n >\r\n {t(\"loginLink\", \"Sign in\")}\r\n </Link>\r\n </div>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport default RegisterPage;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "register-page/lang/en.json",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "related-products-block/related-products-block.tsx",
|
|
20
20
|
"type": "registry:block",
|
|
21
21
|
"target": "$modules$/related-products-block/related-products-block.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\nimport { Star } from \"lucide-react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\nimport { formatPrice } from \"@/modules/ecommerce-core/format-price\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface RelatedProductsBlockProps {\n products: Product[];\n title?: string;\n}\n\nexport function RelatedProductsBlock({\n products,\n title,\n}: RelatedProductsBlockProps) {\n const { t } = useTranslation(\"related-products-block\");\n\n if (products.length === 0) {\n return null;\n }\n\n return (\n <div>\n <h2 className=\"text-2xl font-bold mb-6\">\n {title || t(\"title\", \"Related Products\")}\n </h2>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6\">\n {products.map((product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Card\n className=\"group overflow-hidden border-0 p-0 shadow-sm hover:shadow-lg transition-all duration-300\"\n >\n <Link to={`/products/${product.slug}`}>\n <div className=\"relative aspect-square overflow-hidden cursor-pointer\">\n <img\n src={product.images[0]
|
|
22
|
+
"content": "import { Link } from \"react-router\";\r\nimport { Star } from \"lucide-react\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { formatPrice } from \"@/modules/ecommerce-core/format-price\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface RelatedProductsBlockProps {\r\n products: Product[];\r\n title?: string;\r\n}\r\n\r\nexport function RelatedProductsBlock({\r\n products,\r\n title,\r\n}: RelatedProductsBlockProps) {\r\n const { t } = useTranslation(\"related-products-block\");\r\n\r\n if (products.length === 0) {\r\n return null;\r\n }\r\n\r\n return (\r\n <div>\r\n <h2 className=\"text-2xl font-bold mb-6\">\r\n {title || t(\"title\", \"Related Products\")}\r\n </h2>\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6\">\r\n {products.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Card\r\n className=\"group overflow-hidden border-0 p-0 shadow-sm hover:shadow-lg transition-all duration-300\"\r\n >\r\n <Link to={`/products/${product.slug}`}>\r\n <div className=\"relative aspect-square overflow-hidden cursor-pointer\">\r\n <img\r\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\r\n alt={product.name}\r\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\r\n />\r\n </div>\r\n </Link>\r\n <CardContent className=\"p-4\">\r\n <Link to={`/products/${product.slug}`}>\r\n <h3 className=\"font-semibold hover:text-primary transition-colors line-clamp-1\">\r\n {product.name}\r\n </h3>\r\n </Link>\r\n <div className=\"flex items-center justify-between mt-2\">\r\n <span className=\"font-semibold\">\r\n {formatPrice(product.price, constants.site.currency)}\r\n </span>\r\n <div className=\"flex items-center gap-1\">\r\n <Star className=\"h-3 w-3 fill-current text-yellow-400\" />\r\n <span className=\"text-xs\">{product.rating}</span>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "related-products-block/lang/en.json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promakeai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"promake": "dist/index.js"
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"playground:reset": "bun run playground:create",
|
|
20
20
|
"playground:add": "cd playground && bun run ../src/index.ts add",
|
|
21
21
|
"playground:ecommerce": "rm -rf playground && bun run dev -- create playground --template ecommerce --pm bun",
|
|
22
|
-
"
|
|
22
|
+
"clean": "rimraf dist",
|
|
23
|
+
"build": "bun run clean && bun run build:cli && bun run build:registry",
|
|
23
24
|
"build:cli": "bun build src/index.ts --outdir dist --target node --minify",
|
|
24
25
|
"build:registry": "bun run scripts/build-registry.ts",
|
|
25
26
|
"typecheck": "tsc --noEmit",
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"@types/fs-extra": "^11.0.4",
|
|
50
51
|
"@types/node": "^22.10.2",
|
|
51
52
|
"@types/prompts": "^2.4.9",
|
|
53
|
+
"rimraf": "6.0.1",
|
|
52
54
|
"typescript": "^5.7.2"
|
|
53
55
|
}
|
|
54
56
|
}
|
package/template/README.md
CHANGED
|
@@ -1,73 +1,54 @@
|
|
|
1
|
-
# React
|
|
2
|
-
|
|
3
|
-
This template
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
files: ["**/*.{ts,tsx}"],
|
|
57
|
-
extends: [
|
|
58
|
-
// Other configs...
|
|
59
|
-
// Enable lint rules for React
|
|
60
|
-
reactX.configs["recommended-typescript"],
|
|
61
|
-
// Enable lint rules for React DOM
|
|
62
|
-
reactDom.configs.recommended,
|
|
63
|
-
],
|
|
64
|
-
languageOptions: {
|
|
65
|
-
parserOptions: {
|
|
66
|
-
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
|
67
|
-
tsconfigRootDir: import.meta.dirname,
|
|
68
|
-
},
|
|
69
|
-
// other options...
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
]);
|
|
73
|
-
```
|
|
1
|
+
# Promake React Template
|
|
2
|
+
|
|
3
|
+
This template is the runtime target for `@promakeai/cli`. It includes a
|
|
4
|
+
multi-language, schema-driven database setup powered by `@promakeai/dbreact`.
|
|
5
|
+
|
|
6
|
+
## What You Get
|
|
7
|
+
|
|
8
|
+
- Vite + React + TypeScript
|
|
9
|
+
- Module-based architecture (`src/modules`)
|
|
10
|
+
- Built-in DB layer via `@/db` (dbreact hooks + schema)
|
|
11
|
+
- Multi-language content with translation fallback
|
|
12
|
+
|
|
13
|
+
## Database Layout
|
|
14
|
+
|
|
15
|
+
- Schema: `src/db/schema.json`
|
|
16
|
+
- Types: `src/db/types.ts`
|
|
17
|
+
- Provider: `src/db/provider.tsx`
|
|
18
|
+
- DB file: `public/data/database.db`
|
|
19
|
+
|
|
20
|
+
The app wraps your UI with `AppDbProvider` in `src/App.tsx` and syncs DB language
|
|
21
|
+
with i18n.
|
|
22
|
+
|
|
23
|
+
## Regenerate Database
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# From schema.json
|
|
27
|
+
dbcli generate --schema ./src/db/schema.json \
|
|
28
|
+
--database ./public/data/database.db \
|
|
29
|
+
--output ./src/db
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or run the helper:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun run init-db
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Using the DB in Modules
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useDbList } from "@/db";
|
|
42
|
+
import type { DbProduct } from "@/db";
|
|
43
|
+
|
|
44
|
+
const { data: products } = useDbList<DbProduct>("products", {
|
|
45
|
+
where: { price: { $gt: 50 } },
|
|
46
|
+
limit: 12,
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Language Behavior
|
|
51
|
+
|
|
52
|
+
- `defaultLanguage` is defined in `schema.json`.
|
|
53
|
+
- `DbProvider` uses `lang` + `fallbackLang` for translation resolution.
|
|
54
|
+
- Changing i18n language triggers refetch of db queries.
|
package/template/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promakeai/template",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@hookform/resolvers": "^5.2.2",
|
|
18
|
+
"@promakeai/dbreact": "^1.0.3",
|
|
18
19
|
"@promakeai/customer-backend-client": "^1.1.0",
|
|
19
|
-
"@promakeai/inspector": "^1.
|
|
20
|
+
"@promakeai/inspector": "^1.7.4",
|
|
20
21
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
21
22
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
22
23
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
@@ -91,4 +92,4 @@
|
|
|
91
92
|
"typescript-eslint": "^8.46.4",
|
|
92
93
|
"vite": "^7.2.4"
|
|
93
94
|
}
|
|
94
|
-
}
|
|
95
|
+
}
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -1,131 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import initSqlJs from "sql.js";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const cwd = process.cwd();
|
|
5
|
+
const schemaPath = path.join(cwd, "src", "db", "schema.json");
|
|
6
|
+
const outputDir = path.join(cwd, "src", "db");
|
|
7
|
+
const dbPath = path.join(cwd, "public", "data", "database.db");
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
// BLOG SISTEMI
|
|
11
|
-
// ============================================
|
|
9
|
+
const cmd = `dbcli generate --schema "${schemaPath}" --database "${dbPath}" --output "${outputDir}"`;
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
22
|
-
);
|
|
23
|
-
`);
|
|
24
|
-
|
|
25
|
-
db.exec(`
|
|
26
|
-
CREATE TABLE posts (
|
|
27
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
-
title TEXT NOT NULL,
|
|
29
|
-
slug TEXT UNIQUE NOT NULL,
|
|
30
|
-
content TEXT NOT NULL,
|
|
31
|
-
excerpt TEXT,
|
|
32
|
-
featured_image TEXT,
|
|
33
|
-
images TEXT,
|
|
34
|
-
author TEXT,
|
|
35
|
-
author_avatar TEXT,
|
|
36
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
-
published_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
38
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
39
|
-
tags TEXT,
|
|
40
|
-
read_time INTEGER DEFAULT 0,
|
|
41
|
-
view_count INTEGER DEFAULT 0,
|
|
42
|
-
featured INTEGER DEFAULT 0,
|
|
43
|
-
published INTEGER DEFAULT 1,
|
|
44
|
-
meta_description TEXT,
|
|
45
|
-
meta_keywords TEXT
|
|
46
|
-
);
|
|
47
|
-
`);
|
|
48
|
-
|
|
49
|
-
db.exec(`
|
|
50
|
-
CREATE TABLE post_categories (
|
|
51
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
-
post_id INTEGER NOT NULL,
|
|
53
|
-
category_id INTEGER NOT NULL,
|
|
54
|
-
is_primary INTEGER DEFAULT 0,
|
|
55
|
-
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
|
56
|
-
FOREIGN KEY (category_id) REFERENCES blog_categories(id) ON DELETE CASCADE
|
|
57
|
-
);
|
|
58
|
-
`);
|
|
59
|
-
|
|
60
|
-
// ============================================
|
|
61
|
-
// E-COMMERCE SISTEMI
|
|
62
|
-
// ============================================
|
|
63
|
-
|
|
64
|
-
db.exec(`
|
|
65
|
-
CREATE TABLE product_categories (
|
|
66
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
-
name TEXT NOT NULL,
|
|
68
|
-
slug TEXT UNIQUE NOT NULL,
|
|
69
|
-
description TEXT,
|
|
70
|
-
image TEXT,
|
|
71
|
-
parent_id INTEGER,
|
|
72
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
73
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
74
|
-
FOREIGN KEY (parent_id) REFERENCES product_categories(id) ON DELETE SET NULL
|
|
75
|
-
);
|
|
76
|
-
`);
|
|
77
|
-
|
|
78
|
-
db.exec(`
|
|
79
|
-
CREATE TABLE products (
|
|
80
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
-
name TEXT NOT NULL,
|
|
82
|
-
slug TEXT UNIQUE NOT NULL,
|
|
83
|
-
description TEXT,
|
|
84
|
-
price REAL NOT NULL,
|
|
85
|
-
sale_price REAL,
|
|
86
|
-
on_sale INTEGER DEFAULT 0,
|
|
87
|
-
images TEXT,
|
|
88
|
-
brand TEXT,
|
|
89
|
-
sku TEXT,
|
|
90
|
-
stock INTEGER DEFAULT 0,
|
|
91
|
-
tags TEXT,
|
|
92
|
-
rating REAL DEFAULT 0,
|
|
93
|
-
review_count INTEGER DEFAULT 0,
|
|
94
|
-
featured INTEGER DEFAULT 0,
|
|
95
|
-
is_new INTEGER DEFAULT 0,
|
|
96
|
-
published INTEGER DEFAULT 1,
|
|
97
|
-
specifications TEXT,
|
|
98
|
-
variants TEXT,
|
|
99
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
100
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
101
|
-
meta_description TEXT,
|
|
102
|
-
meta_keywords TEXT
|
|
103
|
-
);
|
|
104
|
-
`);
|
|
105
|
-
|
|
106
|
-
db.exec(`
|
|
107
|
-
CREATE TABLE product_category_relations (
|
|
108
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
109
|
-
product_id INTEGER NOT NULL,
|
|
110
|
-
category_id INTEGER NOT NULL,
|
|
111
|
-
is_primary INTEGER DEFAULT 0,
|
|
112
|
-
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
|
113
|
-
FOREIGN KEY (category_id) REFERENCES product_categories(id) ON DELETE CASCADE
|
|
114
|
-
);
|
|
115
|
-
`);
|
|
116
|
-
|
|
117
|
-
// Database'i dosyaya kaydet
|
|
118
|
-
const data = db.export();
|
|
119
|
-
const buffer = Buffer.from(data);
|
|
120
|
-
|
|
121
|
-
const outputPath = path.join(process.cwd(), "public/data/database.db");
|
|
122
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
123
|
-
fs.writeFileSync(outputPath, buffer);
|
|
124
|
-
|
|
125
|
-
console.log(`Database olusturuldu: ${outputPath}`);
|
|
126
|
-
console.log("Tablolar (6):");
|
|
127
|
-
console.log(" - blog_categories, posts, post_categories");
|
|
128
|
-
console.log(" - product_categories, products, product_category_relations");
|
|
11
|
+
try {
|
|
12
|
+
console.log("Generating database with dbcli...");
|
|
13
|
+
execSync(cmd, { stdio: "inherit" });
|
|
14
|
+
console.log(`Database created: ${dbPath}`);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error("Failed to generate database. Ensure dbcli is installed globally.");
|
|
17
|
+
throw err;
|
|
129
18
|
}
|
|
130
|
-
|
|
131
|
-
createDatabase().catch(console.error);
|
package/template/src/App.tsx
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
2
2
|
import { GoogleAnalytics } from "@/components/GoogleAnalytics";
|
|
3
3
|
import { ScriptInjector } from "@/components/ScriptInjector";
|
|
4
|
+
import { AppDbProvider } from "@/db";
|
|
4
5
|
import { Router } from "./router";
|
|
5
6
|
|
|
6
7
|
const App = () => {
|
|
7
8
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
<AppDbProvider>
|
|
10
|
+
<TooltipProvider>
|
|
11
|
+
<GoogleAnalytics />
|
|
12
|
+
<ScriptInjector />
|
|
13
|
+
<Router />
|
|
14
|
+
</TooltipProvider>
|
|
15
|
+
</AppDbProvider>
|
|
13
16
|
);
|
|
14
17
|
};
|
|
15
18
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { AppDbProvider } from "./provider";
|
|
2
|
+
|
|
3
|
+
import { parseJSONSchema } from "@promakeai/dbreact";
|
|
4
|
+
import schemaJson from "./schema.json";
|
|
5
|
+
export const schema = parseJSONSchema(schemaJson as any);
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
useDb,
|
|
9
|
+
useAdapter,
|
|
10
|
+
useDbLang,
|
|
11
|
+
useDbList,
|
|
12
|
+
useDbGet,
|
|
13
|
+
useDbCreate,
|
|
14
|
+
useDbUpdate,
|
|
15
|
+
useDbDelete,
|
|
16
|
+
SqliteAdapter,
|
|
17
|
+
parseJSONSchema,
|
|
18
|
+
} from "@promakeai/dbreact";
|
|
19
|
+
|
|
20
|
+
export * from "./types";
|