@promakeai/cli 0.9.8 → 0.9.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/README.md +111 -111
- package/dist/index.js +142 -142
- package/dist/registry/about-page.json +3 -3
- package/dist/registry/about-section.json +4 -4
- package/dist/registry/animations.json +2 -2
- package/dist/registry/announcement-bar.json +4 -4
- package/dist/registry/api.json +1 -1
- package/dist/registry/auth-core.json +2 -2
- package/dist/registry/bento-grid-section.json +4 -4
- package/dist/registry/blog-core.json +5 -5
- package/dist/registry/blog-list-page.json +4 -4
- package/dist/registry/blog-section.json +4 -4
- package/dist/registry/cards-carousel-section.json +4 -4
- package/dist/registry/cart-drawer.json +4 -4
- package/dist/registry/cart-page.json +4 -4
- package/dist/registry/case-study-page.json +3 -3
- package/dist/registry/category-section.json +4 -4
- package/dist/registry/checkout-page.json +4 -4
- package/dist/registry/coming-soon-page-minimal.json +4 -4
- package/dist/registry/coming-soon-page.json +4 -4
- package/dist/registry/contact-info-grid.json +4 -4
- package/dist/registry/contact-page-centered.json +4 -4
- package/dist/registry/contact-page-map-overlay.json +3 -3
- package/dist/registry/contact-page-map-split.json +3 -3
- package/dist/registry/contact-page-split.json +4 -4
- package/dist/registry/contact-page.json +4 -4
- package/dist/registry/content-section.json +4 -4
- package/dist/registry/cookie-consent.json +4 -4
- package/dist/registry/cookies-page.json +3 -3
- package/dist/registry/cta-section.json +3 -3
- package/dist/registry/ecommerce-core.json +8 -8
- package/dist/registry/empty-page.json +3 -3
- package/dist/registry/faq-categorized.json +4 -4
- package/dist/registry/faq-simple.json +4 -4
- package/dist/registry/favorites-blog-block.json +1 -1
- package/dist/registry/favorites-blog-page.json +4 -4
- package/dist/registry/favorites-ecommerce-block.json +1 -1
- package/dist/registry/favorites-ecommerce-page.json +4 -4
- package/dist/registry/feature-section.json +3 -3
- package/dist/registry/featured-products.json +4 -4
- package/dist/registry/footer-detailed.json +4 -4
- package/dist/registry/footer-minimal.json +3 -3
- package/dist/registry/footer.json +3 -3
- package/dist/registry/forgot-password-page-split.json +4 -4
- package/dist/registry/forgot-password-page.json +4 -4
- package/dist/registry/google-adsense.json +4 -4
- package/dist/registry/google-map.json +2 -2
- package/dist/registry/header-centered-pill.json +4 -4
- package/dist/registry/header-ecommerce.json +4 -4
- package/dist/registry/header-mega.json +4 -4
- package/dist/registry/header-minimal.json +4 -4
- package/dist/registry/header-simple.json +3 -3
- package/dist/registry/hero-carousel.json +3 -3
- package/dist/registry/hero-cta.json +4 -4
- package/dist/registry/hero-gradient.json +4 -4
- package/dist/registry/hero-grid.json +4 -4
- package/dist/registry/hero-profile.json +3 -3
- package/dist/registry/hero.json +3 -3
- package/dist/registry/index.json +103 -103
- package/dist/registry/landing-page-app.json +3 -3
- package/dist/registry/landing-page-saas.json +3 -3
- package/dist/registry/login-page-split.json +4 -4
- package/dist/registry/login-page.json +4 -4
- package/dist/registry/logo-cloud.json +4 -4
- package/dist/registry/masonry-grid.json +3 -3
- package/dist/registry/my-orders-page.json +4 -4
- package/dist/registry/newsletter-section.json +4 -4
- package/dist/registry/order-card-compact.json +3 -3
- package/dist/registry/order-confirmation-page.json +4 -4
- package/dist/registry/order-detail-block.json +1 -1
- package/dist/registry/orders-list-block.json +1 -1
- package/dist/registry/payment-success-block.json +2 -2
- package/dist/registry/portfolio-page.json +4 -4
- package/dist/registry/post-card.json +4 -4
- package/dist/registry/post-detail-block.json +4 -4
- package/dist/registry/post-detail-page.json +4 -4
- package/dist/registry/pricing-card.json +3 -3
- package/dist/registry/pricing-page.json +4 -4
- package/dist/registry/pricing-section.json +4 -4
- package/dist/registry/privacy-page.json +3 -3
- package/dist/registry/product-card-detailed.json +4 -4
- package/dist/registry/product-card-hover.json +4 -4
- package/dist/registry/product-card.json +4 -4
- package/dist/registry/product-detail-block.json +2 -2
- package/dist/registry/product-detail-page.json +4 -4
- package/dist/registry/product-detail-section.json +4 -4
- package/dist/registry/product-quick-view.json +4 -4
- package/dist/registry/products-page.json +4 -4
- package/dist/registry/reading-progress.json +4 -4
- package/dist/registry/register-page-split.json +4 -4
- package/dist/registry/register-page.json +4 -4
- package/dist/registry/related-posts-block.json +1 -1
- package/dist/registry/related-products-block.json +2 -2
- package/dist/registry/reset-password-page-split.json +4 -4
- package/dist/registry/reset-password-page.json +4 -4
- package/dist/registry/service-card.json +1 -1
- package/dist/registry/share-buttons.json +4 -4
- package/dist/registry/skill-card.json +1 -1
- package/dist/registry/team-page.json +4 -4
- package/dist/registry/terms-page.json +3 -3
- package/dist/registry/testimonials-carousel.json +4 -4
- package/dist/registry/testimonials-grid.json +4 -4
- package/dist/registry/timeline-section.json +4 -4
- package/dist/registry/verify-email-page.json +4 -4
- package/dist/registry/video-hero.json +4 -4
- package/dist/registry/youtube-embed.json +4 -4
- package/package.json +1 -1
- package/template/.env +5 -5
- package/template/README.md +54 -54
- package/template/eslint.config.js +41 -41
- package/template/index.html +237 -237
- package/template/package.json +96 -96
- package/template/public/_redirects +1 -1
- package/template/public/robots.txt +14 -14
- package/template/scripts/init-db.ts +18 -18
- package/template/src/App.tsx +21 -21
- package/template/src/components/FormField.tsx +48 -48
- package/template/src/components/FormFileInput.tsx +75 -75
- package/template/src/components/GoogleAnalytics.tsx +34 -34
- package/template/src/components/LanguageSwitcher.tsx +53 -53
- package/template/src/components/MetriaAnalytics.tsx +68 -68
- package/template/src/components/PasswordInput.tsx +60 -60
- package/template/src/components/ScriptInjector.tsx +62 -62
- package/template/src/components/Stack.tsx +39 -39
- package/template/src/constants/constants.json +71 -71
- package/template/src/db/index.ts +21 -21
- package/template/src/db/provider.tsx +106 -106
- package/template/src/db/schema.json +278 -278
- package/template/src/db/types.ts +195 -195
- package/template/src/hooks/use-debounced-value.ts +12 -12
- package/template/src/hooks/use-page-title.ts +55 -55
- package/template/src/lang/index.ts +90 -90
- package/template/src/lib/api.ts +345 -345
- package/template/src/lib/env.ts +19 -19
- package/template/src/router.tsx +14 -14
- package/template/src/vite-env.d.ts +1 -1
- package/template/vite.config.ts +194 -194
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import { useTranslation } from "react-i18next";
|
|
2
|
-
import { Button } from "@/components/ui/button";
|
|
3
|
-
import {
|
|
4
|
-
DropdownMenu,
|
|
5
|
-
DropdownMenuContent,
|
|
6
|
-
DropdownMenuItem,
|
|
7
|
-
DropdownMenuTrigger,
|
|
8
|
-
} from "@/components/ui/dropdown-menu";
|
|
9
|
-
import { changeLanguage } from "@/lang";
|
|
10
|
-
import { cn } from "@/lib/utils";
|
|
11
|
-
import constants from "@/constants/constants.json";
|
|
12
|
-
|
|
13
|
-
interface LanguageSwitcherProps {
|
|
14
|
-
className?: string;
|
|
15
|
-
style?: React.CSSProperties;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const languages: Record<string, string> =
|
|
19
|
-
constants?.site?.availableLanguages || {};
|
|
20
|
-
|
|
21
|
-
export function LanguageSwitcher({ className, style }: LanguageSwitcherProps) {
|
|
22
|
-
const { i18n } = useTranslation();
|
|
23
|
-
const currentLang = i18n.language;
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<DropdownMenu>
|
|
27
|
-
<DropdownMenuTrigger asChild>
|
|
28
|
-
<Button
|
|
29
|
-
variant="ghost"
|
|
30
|
-
size="sm"
|
|
31
|
-
className={cn("h-9 px-2 text-sm font-medium", className)}
|
|
32
|
-
style={style}
|
|
33
|
-
>
|
|
34
|
-
{languages?.[currentLang] || currentLang.toUpperCase()}
|
|
35
|
-
</Button>
|
|
36
|
-
</DropdownMenuTrigger>
|
|
37
|
-
<DropdownMenuContent align="end">
|
|
38
|
-
{Object.entries(languages).map(([lang, label]) => (
|
|
39
|
-
<DropdownMenuItem
|
|
40
|
-
key={lang}
|
|
41
|
-
onClick={() => changeLanguage(lang)}
|
|
42
|
-
className={cn(
|
|
43
|
-
currentLang === lang ? "bg-accent" : "",
|
|
44
|
-
"hover:text-primary focus:text-primary",
|
|
45
|
-
)}
|
|
46
|
-
>
|
|
47
|
-
{label || lang.toUpperCase()}
|
|
48
|
-
</DropdownMenuItem>
|
|
49
|
-
))}
|
|
50
|
-
</DropdownMenuContent>
|
|
51
|
-
</DropdownMenu>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
1
|
+
import { useTranslation } from "react-i18next";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from "@/components/ui/dropdown-menu";
|
|
9
|
+
import { changeLanguage } from "@/lang";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
import constants from "@/constants/constants.json";
|
|
12
|
+
|
|
13
|
+
interface LanguageSwitcherProps {
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const languages: Record<string, string> =
|
|
19
|
+
constants?.site?.availableLanguages || {};
|
|
20
|
+
|
|
21
|
+
export function LanguageSwitcher({ className, style }: LanguageSwitcherProps) {
|
|
22
|
+
const { i18n } = useTranslation();
|
|
23
|
+
const currentLang = i18n.language;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<DropdownMenu>
|
|
27
|
+
<DropdownMenuTrigger asChild>
|
|
28
|
+
<Button
|
|
29
|
+
variant="ghost"
|
|
30
|
+
size="sm"
|
|
31
|
+
className={cn("h-9 px-2 text-sm font-medium", className)}
|
|
32
|
+
style={style}
|
|
33
|
+
>
|
|
34
|
+
{languages?.[currentLang] || currentLang.toUpperCase()}
|
|
35
|
+
</Button>
|
|
36
|
+
</DropdownMenuTrigger>
|
|
37
|
+
<DropdownMenuContent align="end">
|
|
38
|
+
{Object.entries(languages).map(([lang, label]) => (
|
|
39
|
+
<DropdownMenuItem
|
|
40
|
+
key={lang}
|
|
41
|
+
onClick={() => changeLanguage(lang)}
|
|
42
|
+
className={cn(
|
|
43
|
+
currentLang === lang ? "bg-accent" : "",
|
|
44
|
+
"hover:text-primary focus:text-primary",
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{label || lang.toUpperCase()}
|
|
48
|
+
</DropdownMenuItem>
|
|
49
|
+
))}
|
|
50
|
+
</DropdownMenuContent>
|
|
51
|
+
</DropdownMenu>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
const DEFAULT_TRACKER_SCRIPT_URL =
|
|
3
|
-
'https://cdn.jsdelivr.net/npm/@litemetrics/tracker@latest/dist/litemetrics.global.js';
|
|
4
|
-
|
|
5
|
-
function pickFirstNonEmpty(...values: Array<string | undefined>): string | undefined {
|
|
6
|
-
return values.find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const analyticsConfig = {
|
|
10
|
-
siteId: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SITE_ID, import.meta.env.VITE_METRIA_SITE_ID),
|
|
11
|
-
serverUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SERVER_URL, import.meta.env.VITE_METRIA_SERVER_URL),
|
|
12
|
-
trackerScriptUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SCRIPT_URL) ?? DEFAULT_TRACKER_SCRIPT_URL,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const isAnalyticsEnabled = Boolean(analyticsConfig.siteId && analyticsConfig.serverUrl);
|
|
16
|
-
|
|
17
|
-
type TrackerGlobal = {
|
|
18
|
-
createTracker: (config: { siteId: string; endpoint: string }) => void;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
declare global {
|
|
22
|
-
interface Window {
|
|
23
|
-
Litemetrics?: TrackerGlobal;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stripTrailingSlashes(value: string): string {
|
|
28
|
-
return value.replace(/\/+$/, '');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function ensureHttps(value: string): string {
|
|
32
|
-
return value.replace(/^http:\/\//i, 'https://');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function MetriaAnalytics() {
|
|
36
|
-
const injected = useRef(false);
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (!isAnalyticsEnabled || !analyticsConfig.siteId || !analyticsConfig.serverUrl || injected.current) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (document.querySelector('[data-injected="litemetrics-tracker"]')) return;
|
|
43
|
-
|
|
44
|
-
injected.current = true;
|
|
45
|
-
|
|
46
|
-
const normalizedServerUrl = ensureHttps(stripTrailingSlashes(analyticsConfig.serverUrl));
|
|
47
|
-
const script = document.createElement('script');
|
|
48
|
-
script.async = true;
|
|
49
|
-
script.src = analyticsConfig.trackerScriptUrl;
|
|
50
|
-
script.setAttribute('data-injected', 'litemetrics-tracker');
|
|
51
|
-
script.onload = () => {
|
|
52
|
-
if (document.querySelector('[data-injected="litemetrics-init"]')) return;
|
|
53
|
-
|
|
54
|
-
window.Litemetrics?.createTracker({
|
|
55
|
-
siteId: analyticsConfig.siteId!,
|
|
56
|
-
endpoint: `${normalizedServerUrl}/api/collect`,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const marker = document.createElement('meta');
|
|
60
|
-
marker.setAttribute('data-injected', 'litemetrics-init');
|
|
61
|
-
document.head.appendChild(marker);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
document.head.appendChild(script);
|
|
65
|
-
}, []);
|
|
66
|
-
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
const DEFAULT_TRACKER_SCRIPT_URL =
|
|
3
|
+
'https://cdn.jsdelivr.net/npm/@litemetrics/tracker@latest/dist/litemetrics.global.js';
|
|
4
|
+
|
|
5
|
+
function pickFirstNonEmpty(...values: Array<string | undefined>): string | undefined {
|
|
6
|
+
return values.find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const analyticsConfig = {
|
|
10
|
+
siteId: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SITE_ID, import.meta.env.VITE_METRIA_SITE_ID),
|
|
11
|
+
serverUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SERVER_URL, import.meta.env.VITE_METRIA_SERVER_URL),
|
|
12
|
+
trackerScriptUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SCRIPT_URL) ?? DEFAULT_TRACKER_SCRIPT_URL,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const isAnalyticsEnabled = Boolean(analyticsConfig.siteId && analyticsConfig.serverUrl);
|
|
16
|
+
|
|
17
|
+
type TrackerGlobal = {
|
|
18
|
+
createTracker: (config: { siteId: string; endpoint: string }) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Window {
|
|
23
|
+
Litemetrics?: TrackerGlobal;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stripTrailingSlashes(value: string): string {
|
|
28
|
+
return value.replace(/\/+$/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureHttps(value: string): string {
|
|
32
|
+
return value.replace(/^http:\/\//i, 'https://');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MetriaAnalytics() {
|
|
36
|
+
const injected = useRef(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isAnalyticsEnabled || !analyticsConfig.siteId || !analyticsConfig.serverUrl || injected.current) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (document.querySelector('[data-injected="litemetrics-tracker"]')) return;
|
|
43
|
+
|
|
44
|
+
injected.current = true;
|
|
45
|
+
|
|
46
|
+
const normalizedServerUrl = ensureHttps(stripTrailingSlashes(analyticsConfig.serverUrl));
|
|
47
|
+
const script = document.createElement('script');
|
|
48
|
+
script.async = true;
|
|
49
|
+
script.src = analyticsConfig.trackerScriptUrl;
|
|
50
|
+
script.setAttribute('data-injected', 'litemetrics-tracker');
|
|
51
|
+
script.onload = () => {
|
|
52
|
+
if (document.querySelector('[data-injected="litemetrics-init"]')) return;
|
|
53
|
+
|
|
54
|
+
window.Litemetrics?.createTracker({
|
|
55
|
+
siteId: analyticsConfig.siteId!,
|
|
56
|
+
endpoint: `${normalizedServerUrl}/api/collect`,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const marker = document.createElement('meta');
|
|
60
|
+
marker.setAttribute('data-injected', 'litemetrics-init');
|
|
61
|
+
document.head.appendChild(marker);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
document.head.appendChild(script);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
@@ -1,61 +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
|
-
);
|
|
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
61
|
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
import constants from "@/constants/constants.json";
|
|
3
|
-
|
|
4
|
-
function injectScript(html: string, target: "head" | "body", position: "start" | "end", marker: string) {
|
|
5
|
-
if (!html) return;
|
|
6
|
-
|
|
7
|
-
// Check if already injected
|
|
8
|
-
if (document.querySelector(`[data-injected="${marker}"]`)) return;
|
|
9
|
-
|
|
10
|
-
const container = document.createElement("div");
|
|
11
|
-
container.innerHTML = html;
|
|
12
|
-
|
|
13
|
-
const targetElement = target === "head" ? document.head : document.body;
|
|
14
|
-
|
|
15
|
-
// Create wrapper with marker
|
|
16
|
-
const wrapper = document.createDocumentFragment();
|
|
17
|
-
|
|
18
|
-
Array.from(container.childNodes).forEach((node) => {
|
|
19
|
-
if (node.nodeName === "SCRIPT") {
|
|
20
|
-
// Recreate script for execution
|
|
21
|
-
const script = node as HTMLScriptElement;
|
|
22
|
-
const newScript = document.createElement("script");
|
|
23
|
-
newScript.setAttribute("data-injected", marker);
|
|
24
|
-
Array.from(script.attributes).forEach((attr) => {
|
|
25
|
-
newScript.setAttribute(attr.name, attr.value);
|
|
26
|
-
});
|
|
27
|
-
if (script.innerHTML) {
|
|
28
|
-
newScript.innerHTML = script.innerHTML;
|
|
29
|
-
}
|
|
30
|
-
wrapper.appendChild(newScript);
|
|
31
|
-
} else {
|
|
32
|
-
const clone = node.cloneNode(true) as HTMLElement;
|
|
33
|
-
if (clone.setAttribute) {
|
|
34
|
-
clone.setAttribute("data-injected", marker);
|
|
35
|
-
}
|
|
36
|
-
wrapper.appendChild(clone);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (position === "start") {
|
|
41
|
-
targetElement.insertBefore(wrapper, targetElement.firstChild);
|
|
42
|
-
} else {
|
|
43
|
-
targetElement.appendChild(wrapper);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function ScriptInjector() {
|
|
48
|
-
const injected = useRef(false);
|
|
49
|
-
const { headStart, headEnd, bodyStart, bodyEnd } = constants.scripts;
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (injected.current) return;
|
|
53
|
-
injected.current = true;
|
|
54
|
-
|
|
55
|
-
injectScript(headStart, "head", "start", "head-start");
|
|
56
|
-
injectScript(headEnd, "head", "end", "head-end");
|
|
57
|
-
injectScript(bodyStart, "body", "start", "body-start");
|
|
58
|
-
injectScript(bodyEnd, "body", "end", "body-end");
|
|
59
|
-
}, []);
|
|
60
|
-
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import constants from "@/constants/constants.json";
|
|
3
|
+
|
|
4
|
+
function injectScript(html: string, target: "head" | "body", position: "start" | "end", marker: string) {
|
|
5
|
+
if (!html) return;
|
|
6
|
+
|
|
7
|
+
// Check if already injected
|
|
8
|
+
if (document.querySelector(`[data-injected="${marker}"]`)) return;
|
|
9
|
+
|
|
10
|
+
const container = document.createElement("div");
|
|
11
|
+
container.innerHTML = html;
|
|
12
|
+
|
|
13
|
+
const targetElement = target === "head" ? document.head : document.body;
|
|
14
|
+
|
|
15
|
+
// Create wrapper with marker
|
|
16
|
+
const wrapper = document.createDocumentFragment();
|
|
17
|
+
|
|
18
|
+
Array.from(container.childNodes).forEach((node) => {
|
|
19
|
+
if (node.nodeName === "SCRIPT") {
|
|
20
|
+
// Recreate script for execution
|
|
21
|
+
const script = node as HTMLScriptElement;
|
|
22
|
+
const newScript = document.createElement("script");
|
|
23
|
+
newScript.setAttribute("data-injected", marker);
|
|
24
|
+
Array.from(script.attributes).forEach((attr) => {
|
|
25
|
+
newScript.setAttribute(attr.name, attr.value);
|
|
26
|
+
});
|
|
27
|
+
if (script.innerHTML) {
|
|
28
|
+
newScript.innerHTML = script.innerHTML;
|
|
29
|
+
}
|
|
30
|
+
wrapper.appendChild(newScript);
|
|
31
|
+
} else {
|
|
32
|
+
const clone = node.cloneNode(true) as HTMLElement;
|
|
33
|
+
if (clone.setAttribute) {
|
|
34
|
+
clone.setAttribute("data-injected", marker);
|
|
35
|
+
}
|
|
36
|
+
wrapper.appendChild(clone);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (position === "start") {
|
|
41
|
+
targetElement.insertBefore(wrapper, targetElement.firstChild);
|
|
42
|
+
} else {
|
|
43
|
+
targetElement.appendChild(wrapper);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ScriptInjector() {
|
|
48
|
+
const injected = useRef(false);
|
|
49
|
+
const { headStart, headEnd, bodyStart, bodyEnd } = constants.scripts;
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (injected.current) return;
|
|
53
|
+
injected.current = true;
|
|
54
|
+
|
|
55
|
+
injectScript(headStart, "head", "start", "head-start");
|
|
56
|
+
injectScript(headEnd, "head", "end", "head-end");
|
|
57
|
+
injectScript(bodyStart, "body", "start", "body-start");
|
|
58
|
+
injectScript(bodyEnd, "body", "end", "body-end");
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import clsx from "clsx";
|
|
2
|
-
import { Children } from "react";
|
|
3
|
-
|
|
4
|
-
type StackProps = {
|
|
5
|
-
as?: keyof HTMLElementTagNameMap; // "div", "ul", "section" vs can be any valid HTML element
|
|
6
|
-
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; //Tailwind spacing scale
|
|
7
|
-
direction?: "vertical" | "horizontal";
|
|
8
|
-
className?: string;
|
|
9
|
-
children: React.ReactNode;
|
|
10
|
-
wrapChildren?: boolean; // If true, wraps each child in a Stack with the same gap
|
|
11
|
-
childGap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; // Gap for wrapped children
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function Stack({
|
|
15
|
-
as: Component = "div",
|
|
16
|
-
gap = 2,
|
|
17
|
-
direction = "vertical",
|
|
18
|
-
className,
|
|
19
|
-
children,
|
|
20
|
-
wrapChildren = false,
|
|
21
|
-
childGap = 2,
|
|
22
|
-
}: StackProps) {
|
|
23
|
-
const gapClass =
|
|
24
|
-
direction === "vertical" ? `space-y-${gap}` : `space-x-${gap}`;
|
|
25
|
-
|
|
26
|
-
const content = wrapChildren
|
|
27
|
-
? Children.map(children, (child, index) => (
|
|
28
|
-
<Stack key={index} gap={childGap} direction={direction}>
|
|
29
|
-
{child}
|
|
30
|
-
</Stack>
|
|
31
|
-
))
|
|
32
|
-
: children;
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Component className={clsx(gapClass, className)}>
|
|
36
|
-
{content}
|
|
37
|
-
</Component>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { Children } from "react";
|
|
3
|
+
|
|
4
|
+
type StackProps = {
|
|
5
|
+
as?: keyof HTMLElementTagNameMap; // "div", "ul", "section" vs can be any valid HTML element
|
|
6
|
+
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; //Tailwind spacing scale
|
|
7
|
+
direction?: "vertical" | "horizontal";
|
|
8
|
+
className?: string;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
wrapChildren?: boolean; // If true, wraps each child in a Stack with the same gap
|
|
11
|
+
childGap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; // Gap for wrapped children
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Stack({
|
|
15
|
+
as: Component = "div",
|
|
16
|
+
gap = 2,
|
|
17
|
+
direction = "vertical",
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
wrapChildren = false,
|
|
21
|
+
childGap = 2,
|
|
22
|
+
}: StackProps) {
|
|
23
|
+
const gapClass =
|
|
24
|
+
direction === "vertical" ? `space-y-${gap}` : `space-x-${gap}`;
|
|
25
|
+
|
|
26
|
+
const content = wrapChildren
|
|
27
|
+
? Children.map(children, (child, index) => (
|
|
28
|
+
<Stack key={index} gap={childGap} direction={direction}>
|
|
29
|
+
{child}
|
|
30
|
+
</Stack>
|
|
31
|
+
))
|
|
32
|
+
: children;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Component className={clsx(gapClass, className)}>
|
|
36
|
+
{content}
|
|
37
|
+
</Component>
|
|
38
|
+
);
|
|
39
|
+
}
|