@promakeai/cli 0.4.9 → 0.4.10
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 +183 -176
- package/dist/registry/blog-core.json +26 -7
- 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/contact-page-centered.json +1 -1
- package/dist/registry/contact-page-map-overlay.json +1 -1
- package/dist/registry/contact-page-map-split.json +1 -1
- package/dist/registry/contact-page-split.json +1 -1
- package/dist/registry/contact-page.json +1 -1
- package/dist/registry/db.json +129 -0
- package/dist/registry/docs/blog-core.md +13 -12
- package/dist/registry/docs/blog-list-page.md +1 -1
- package/dist/registry/docs/ecommerce-core.md +13 -10
- 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 +25 -5
- package/dist/registry/featured-products.json +2 -2
- package/dist/registry/forgot-password-page-split.json +1 -1
- package/dist/registry/forgot-password-page.json +1 -1
- package/dist/registry/header-centered-pill.json +1 -1
- package/dist/registry/header-ecommerce.json +1 -1
- package/dist/registry/index.json +1 -0
- package/dist/registry/login-page-split.json +1 -1
- package/dist/registry/login-page.json +1 -1
- package/dist/registry/newsletter-section.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-split.json +1 -1
- package/dist/registry/register-page.json +1 -1
- package/dist/registry/related-products-block.json +1 -1
- package/dist/registry/reset-password-page-split.json +1 -1
- package/package.json +2 -4
- package/template/README.md +58 -39
- package/template/eslint.config.js +37 -37
- package/template/package.json +3 -4
- package/template/public/data/database.db +0 -0
- package/template/scripts/init-db.ts +126 -13
- package/template/src/App.tsx +5 -8
- package/template/src/PasswordInput.tsx +61 -0
- package/template/src/components/FormField.tsx +11 -5
- package/template/src/lang/index.ts +86 -86
- package/README.md +0 -71
- package/template/public/data/database.db-shm +0 -0
- package/template/public/data/database.db-wal +0 -0
- package/template/src/db/index.ts +0 -20
- package/template/src/db/provider.tsx +0 -77
- package/template/src/db/schema.json +0 -259
- package/template/src/db/types.ts +0 -195
- package/template/src/hooks/use-debounced-value.ts +0 -12
|
@@ -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 <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"
|
|
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 { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { UserPlus, CheckCircle } from \"lucide-react\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\nimport { FormField } from \"@/components/FormField\";\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 [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 <FormField label={t(\"username\", \"Username\")} htmlFor=\"username\" required>\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 </FormField>\r\n <FormField label={t(\"email\", \"Email\")} htmlFor=\"email\" required>\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 </FormField>\r\n <FormField label={t(\"password\", \"Password\")} htmlFor=\"password\" required>\r\n <PasswordInput id=\"password\"\r\n name=\"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 </FormField>\r\n\r\n <FormField label={t(\"confirmPassword\", \"Confirm Password\")} htmlFor=\"confirmPassword\" required\r\n >\r\n <PasswordInput\r\n id=\"confirmPassword\"\r\n name=\"confirmPassword\"\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 />\r\n </FormField>\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"
|
|
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\";\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
|
|
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[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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"path": "reset-password-page-split/reset-password-page-split.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/reset-password-page-split/reset-password-page-split.tsx",
|
|
26
|
-
"content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useSearchParams } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { KeyRound, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface ResetPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ResetPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ResetPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"reset-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Reset Password\") });\r\n const navigate = useNavigate();\r\n const [searchParams] = useSearchParams();\r\n const { resetPassword } = useAuth();\r\n\r\n // Get code and username from URL params\r\n const code = searchParams.get(\"code\") || \"\";\r\n const username = searchParams.get(\"username\") || searchParams.get(\"email\") || \"\";\r\n\r\n const [password, setPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isSuccess, setIsSuccess] = useState(false);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (password !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n // Validate we have required params\r\n if (!code || !username) {\r\n setError(t(\"invalidLink\", \"Invalid or expired reset link. Please request a new one.\"));\r\n return;\r\n }\r\n\r\n setIsLoading(true);\r\n\r\n try {\r\n await resetPassword(username, code, password);\r\n\r\n setIsSuccess(true);\r\n toast.success(t(\"passwordResetSuccess\", \"Password reset successfully!\"));\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"resetPasswordError\", \"Failed to reset password. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state\r\n if (isSuccess) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"passwordReset\", \"Password Reset!\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"passwordResetDescription\", \"Your password has been reset successfully. You can now log in with your new password.\")}\r\n </p>\r\n </div>\r\n\r\n <Button onClick={() => navigate(\"/login\")} className=\"w-full\">\r\n {t(\"goToLogin\", \"Go to Login\")}\r\n </Button>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Reset password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n // Invalid link state\r\n if (!code || !username) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight text-red-600\">\r\n {t(\"invalidLinkTitle\", \"Invalid Reset Link\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"invalidLinkDescription\", \"This password reset link is invalid or has expired. Please request a new one.\")}\r\n </p>\r\n </div>\r\n\r\n <Button onClick={() => navigate(\"/forgot-password\")} className=\"w-full\">\r\n {t(\"requestNewLink\", \"Request New Link\")}\r\n </Button>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Reset password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <KeyRound className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Reset Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your new password below\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"password\">{t(\"newPassword\", \"New Password\")}</Label>\r\n <Input\r\n required\r\n id=\"password\"\r\n type=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\r\n />\r\n </div>\r\n\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"confirm-password\">{t(\"confirmPassword\", \"Confirm Password\")}</Label>\r\n <Input\r\n required\r\n id=\"confirm-password\"\r\n type=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={confirmPassword}\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Reset password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ResetPasswordPageSplit;\r\n"
|
|
26
|
+
"content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useSearchParams } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { KeyRound, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { FormField } from \"@/components/FormField\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\n\r\ninterface ResetPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ResetPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ResetPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"reset-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Reset Password\") });\r\n const navigate = useNavigate();\r\n const [searchParams] = useSearchParams();\r\n const { resetPassword } = useAuth();\r\n\r\n // Get code and username from URL params\r\n const code = searchParams.get(\"code\") || \"\";\r\n const username = searchParams.get(\"username\") || searchParams.get(\"email\") || \"\";\r\n\r\n const [password, setPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isSuccess, setIsSuccess] = useState(false);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (password !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n // Validate we have required params\r\n if (!code || !username) {\r\n setError(t(\"invalidLink\", \"Invalid or expired reset link. Please request a new one.\"));\r\n return;\r\n }\r\n\r\n setIsLoading(true);\r\n\r\n try {\r\n await resetPassword(username, code, password);\r\n\r\n setIsSuccess(true);\r\n toast.success(t(\"passwordResetSuccess\", \"Password reset successfully!\"));\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"resetPasswordError\", \"Failed to reset password. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state\r\n if (isSuccess) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"passwordReset\", \"Password Reset!\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"passwordResetDescription\", \"Your password has been reset successfully. You can now log in with your new password.\")}\r\n </p>\r\n </div>\r\n\r\n <Button onClick={() => navigate(\"/login\")} className=\"w-full\">\r\n {t(\"goToLogin\", \"Go to Login\")}\r\n </Button>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Reset password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n // Invalid link state\r\n if (!code || !username) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight text-red-600\">\r\n {t(\"invalidLinkTitle\", \"Invalid Reset Link\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"invalidLinkDescription\", \"This password reset link is invalid or has expired. Please request a new one.\")}\r\n </p>\r\n </div>\r\n\r\n <Button onClick={() => navigate(\"/forgot-password\")} className=\"w-full\">\r\n {t(\"requestNewLink\", \"Request New Link\")}\r\n </Button>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Reset password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n } \r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <KeyRound className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Reset Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your new password below\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <FormField label={t(\"newPassword\", \"New Password\")} htmlFor=\"password\" required>\r\n <PasswordInput\r\n required\r\n id=\"password\"\r\n name=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <div className=\"grid gap-2\">\r\n <FormField label={t(\"confirmPassword\", \"Confirm Password\")} htmlFor=\"confirm-password\" required>\r\n <PasswordInput\r\n required\r\n id=\"confirm-password\"\r\n name=\"confirm-password\"\r\n autoComplete=\"confirm-password\"\r\n placeholder=\"••••••••\"\r\n value={confirmPassword}\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Reset password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ResetPasswordPageSplit;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "reset-password-page-split/lang/en.json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promakeai/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"promake": "dist/index.js"
|
|
@@ -19,8 +19,7 @@
|
|
|
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
|
-
"
|
|
23
|
-
"build": "bun run clean && bun run build:cli && bun run build:registry",
|
|
22
|
+
"build": "bun run build:cli && bun run build:registry",
|
|
24
23
|
"build:cli": "bun build src/index.ts --outdir dist --target node --minify",
|
|
25
24
|
"build:registry": "bun run scripts/build-registry.ts",
|
|
26
25
|
"typecheck": "tsc --noEmit",
|
|
@@ -50,7 +49,6 @@
|
|
|
50
49
|
"@types/fs-extra": "^11.0.4",
|
|
51
50
|
"@types/node": "^22.10.2",
|
|
52
51
|
"@types/prompts": "^2.4.9",
|
|
53
|
-
"rimraf": "6.0.1",
|
|
54
52
|
"typescript": "^5.7.2"
|
|
55
53
|
}
|
|
56
54
|
}
|
package/template/README.md
CHANGED
|
@@ -1,54 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
2
|
|
|
3
|
-
This template
|
|
4
|
-
multi-language, schema-driven database setup powered by `@promakeai/dbreact`.
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
Currently, two official plugins are available:
|
|
7
6
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Built-in DB layer via `@/db` (dbreact hooks + schema)
|
|
11
|
-
- Multi-language content with translation fallback
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
12
9
|
|
|
13
|
-
##
|
|
10
|
+
## React Compiler
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
- Types: `src/db/types.ts`
|
|
17
|
-
- Provider: `src/db/provider.tsx`
|
|
18
|
-
- DB file: `public/data/database.db`
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
with i18n.
|
|
14
|
+
## Expanding the ESLint configuration
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
24
17
|
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(["dist"]),
|
|
21
|
+
{
|
|
22
|
+
files: ["**/*.{ts,tsx}"],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
36
44
|
```
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
39
47
|
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
import
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from "eslint-plugin-react-x";
|
|
51
|
+
import reactDom from "eslint-plugin-react-dom";
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(["dist"]),
|
|
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
|
+
]);
|
|
48
73
|
```
|
|
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.
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import js from '@eslint/js';
|
|
2
|
-
import globals from 'globals';
|
|
3
|
-
import reactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
-
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
5
|
-
import tseslint from 'typescript-eslint';
|
|
6
|
-
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
7
|
-
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
|
8
|
-
|
|
9
|
-
export default defineConfig([
|
|
10
|
-
globalIgnores(['dist']),
|
|
11
|
-
{
|
|
12
|
-
files: ['**/*.{ts,tsx}'],
|
|
13
|
-
extends: [
|
|
14
|
-
js.configs.recommended,
|
|
15
|
-
tseslint.configs.recommended,
|
|
16
|
-
reactHooks.configs.flat.recommended,
|
|
17
|
-
reactRefresh.configs.vite,
|
|
18
|
-
eslintConfigPrettier,
|
|
19
|
-
],
|
|
20
|
-
languageOptions: {
|
|
21
|
-
ecmaVersion: 2020,
|
|
22
|
-
globals: globals.browser,
|
|
23
|
-
},
|
|
24
|
-
rules: {
|
|
25
|
-
'@typescript-eslint/no-explicit-any': 'off',
|
|
26
|
-
'@typescript-eslint/no-unused-vars': 'off',
|
|
27
|
-
'@typescript-eslint/no-unused-expressions': 'off',
|
|
28
|
-
'react-refresh/only-export-components': 'off',
|
|
29
|
-
'react-hooks/purity': 'off',
|
|
30
|
-
'react-hooks/exhaustive-deps': 'off',
|
|
31
|
-
'react-hooks/set-state-in-effect': 'off',
|
|
32
|
-
'no-unused-vars': 'off',
|
|
33
|
-
'prefer-const': 'warn',
|
|
34
|
-
'@typescript-eslint/no-empty-object-type': 'off',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
]);
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
7
|
+
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
|
8
|
+
|
|
9
|
+
export default defineConfig([
|
|
10
|
+
globalIgnores(['dist']),
|
|
11
|
+
{
|
|
12
|
+
files: ['**/*.{ts,tsx}'],
|
|
13
|
+
extends: [
|
|
14
|
+
js.configs.recommended,
|
|
15
|
+
tseslint.configs.recommended,
|
|
16
|
+
reactHooks.configs.flat.recommended,
|
|
17
|
+
reactRefresh.configs.vite,
|
|
18
|
+
eslintConfigPrettier,
|
|
19
|
+
],
|
|
20
|
+
languageOptions: {
|
|
21
|
+
ecmaVersion: 2020,
|
|
22
|
+
globals: globals.browser,
|
|
23
|
+
},
|
|
24
|
+
rules: {
|
|
25
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
26
|
+
'@typescript-eslint/no-unused-vars': 'off',
|
|
27
|
+
'@typescript-eslint/no-unused-expressions': 'off',
|
|
28
|
+
'react-refresh/only-export-components': 'off',
|
|
29
|
+
'react-hooks/purity': 'off',
|
|
30
|
+
'react-hooks/exhaustive-deps': 'off',
|
|
31
|
+
'react-hooks/set-state-in-effect': 'off',
|
|
32
|
+
'no-unused-vars': 'off',
|
|
33
|
+
'prefer-const': 'warn',
|
|
34
|
+
'@typescript-eslint/no-empty-object-type': 'off',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
]);
|
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.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@hookform/resolvers": "^5.2.2",
|
|
18
|
-
"@promakeai/dbreact": "^1.0.4",
|
|
19
18
|
"@promakeai/customer-backend-client": "^1.1.0",
|
|
20
|
-
"@promakeai/inspector": "^1.
|
|
19
|
+
"@promakeai/inspector": "^1.5.1",
|
|
21
20
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
22
21
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
23
22
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
@@ -92,4 +91,4 @@
|
|
|
92
91
|
"typescript-eslint": "^8.46.4",
|
|
93
92
|
"vite": "^7.2.4"
|
|
94
93
|
}
|
|
95
|
-
}
|
|
94
|
+
}
|
|
Binary file
|
|
@@ -1,18 +1,131 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import initSqlJs from "sql.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const dbPath = path.join(cwd, "public", "data", "database.db");
|
|
5
|
+
async function createDatabase() {
|
|
6
|
+
const SQL = await initSqlJs();
|
|
7
|
+
const db = new SQL.Database();
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// ============================================
|
|
10
|
+
// BLOG SISTEMI
|
|
11
|
+
// ============================================
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
db.exec(`
|
|
14
|
+
CREATE TABLE blog_categories (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
name TEXT NOT NULL,
|
|
17
|
+
slug TEXT UNIQUE NOT NULL,
|
|
18
|
+
description TEXT,
|
|
19
|
+
image TEXT,
|
|
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");
|
|
18
129
|
}
|
|
130
|
+
|
|
131
|
+
createDatabase().catch(console.error);
|
package/template/src/App.tsx
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
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";
|
|
5
4
|
import { Router } from "./router";
|
|
6
5
|
|
|
7
6
|
const App = () => {
|
|
8
7
|
return (
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</TooltipProvider>
|
|
15
|
-
</AppDbProvider>
|
|
8
|
+
<TooltipProvider>
|
|
9
|
+
<GoogleAnalytics />
|
|
10
|
+
<ScriptInjector />
|
|
11
|
+
<Router />
|
|
12
|
+
</TooltipProvider>
|
|
16
13
|
);
|
|
17
14
|
};
|
|
18
15
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Input } from "@/components/ui/input";
|
|
3
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface PasswordInputProps {
|
|
6
|
+
id?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
autoComplete?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
minLength?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function PasswordInput({
|
|
19
|
+
id = "password",
|
|
20
|
+
name = "password",
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
placeholder = "Enter password",
|
|
24
|
+
required = false,
|
|
25
|
+
autoComplete = "current-password",
|
|
26
|
+
disabled = false,
|
|
27
|
+
minLength = undefined,
|
|
28
|
+
className = "",
|
|
29
|
+
}: PasswordInputProps) {
|
|
30
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="relative">
|
|
34
|
+
<Input
|
|
35
|
+
id={id}
|
|
36
|
+
name={name}
|
|
37
|
+
type={showPassword ? "text" : "password"}
|
|
38
|
+
value={value}
|
|
39
|
+
onChange={onChange}
|
|
40
|
+
placeholder={placeholder}
|
|
41
|
+
required={required}
|
|
42
|
+
className={`mt-1 pr-10 ${className}`}
|
|
43
|
+
autoComplete={autoComplete}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
minLength={minLength}
|
|
46
|
+
/>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
50
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
51
|
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
52
|
+
>
|
|
53
|
+
{showPassword ? (
|
|
54
|
+
<EyeOff className="w-4 h-4" />
|
|
55
|
+
) : (
|
|
56
|
+
<Eye className="w-4 h-4" />
|
|
57
|
+
)}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|