fix: WP-Admin CSS conflicts and add-to-cart redirect
- Fix CSS conflicts between WP-Admin and SPA (radio buttons, chart text) - Add Tailwind important selector scoped to #woonoow-admin-app - Remove overly aggressive inline SVG styles from Assets.php - Add targeted WordPress admin CSS overrides in index.css - Fix add-to-cart redirect to use woocommerce_add_to_cart_redirect filter - Let WooCommerce handle cart operations natively for proper session management - Remove duplicate tailwind.config.cjs
This commit is contained in:
@@ -127,6 +127,7 @@ export default function ProductEdit() {
|
||||
onSubmit={handleSubmit}
|
||||
formRef={formRef}
|
||||
hideSubmitButton={true}
|
||||
productId={product.id}
|
||||
/>
|
||||
|
||||
{/* Level 1 compatibility: Custom meta fields from plugins */}
|
||||
|
||||
215
admin-spa/src/routes/Products/partials/DirectCartLinks.tsx
Normal file
215
admin-spa/src/routes/Products/partials/DirectCartLinks.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Copy, Check, ExternalLink } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface DirectCartLinksProps {
|
||||
productId: number;
|
||||
productType: 'simple' | 'variable';
|
||||
variations?: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
attributes: Record<string, string>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function DirectCartLinks({ productId, productType, variations = [] }: DirectCartLinksProps) {
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
||||
|
||||
const siteUrl = window.location.origin;
|
||||
const spaPagePath = '/store'; // This should ideally come from settings
|
||||
|
||||
const generateLink = (variationId?: number, redirect: 'cart' | 'checkout' = 'cart') => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('add-to-cart', productId.toString());
|
||||
if (variationId) {
|
||||
params.set('variation_id', variationId.toString());
|
||||
}
|
||||
if (quantity > 1) {
|
||||
params.set('quantity', quantity.toString());
|
||||
}
|
||||
params.set('redirect', redirect);
|
||||
|
||||
return `${siteUrl}${spaPagePath}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (link: string, label: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(link);
|
||||
setCopiedLink(link);
|
||||
toast.success(`${label} link copied!`);
|
||||
setTimeout(() => setCopiedLink(null), 2000);
|
||||
} catch (err) {
|
||||
toast.error('Failed to copy link');
|
||||
}
|
||||
};
|
||||
|
||||
const LinkRow = ({
|
||||
label,
|
||||
link,
|
||||
description
|
||||
}: {
|
||||
label: string;
|
||||
link: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const isCopied = copiedLink === link;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">{label}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(link, label)}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => window.open(link, '_blank')}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={link}
|
||||
readOnly
|
||||
className="font-mono text-xs"
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Direct-to-Cart Links</CardTitle>
|
||||
<CardDescription>
|
||||
Generate copyable links that add this product to cart and redirect to cart or checkout page.
|
||||
Perfect for landing pages, email campaigns, and social media.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Quantity Selector */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="link-quantity">Default Quantity</Label>
|
||||
<Input
|
||||
id="link-quantity"
|
||||
type="number"
|
||||
min="1"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
|
||||
className="w-32"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Set quantity to 1 to exclude from URL (cleaner links)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Simple Product Links */}
|
||||
{productType === 'simple' && (
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h4 className="font-medium">Simple Product Links</h4>
|
||||
</div>
|
||||
|
||||
<LinkRow
|
||||
label="Add to Cart"
|
||||
link={generateLink(undefined, 'cart')}
|
||||
description="Adds product to cart and shows cart page"
|
||||
/>
|
||||
|
||||
<LinkRow
|
||||
label="Direct to Checkout"
|
||||
link={generateLink(undefined, 'checkout')}
|
||||
description="Adds product to cart and goes directly to checkout"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Variable Product Links */}
|
||||
{productType === 'variable' && variations.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h4 className="font-medium">Variable Product Links</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{variations.length} variation(s) - Select a variation to generate links
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{variations.map((variation, index) => (
|
||||
<details key={variation.id} className="group border rounded-lg">
|
||||
<summary className="cursor-pointer p-3 hover:bg-muted/50 flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-sm">{variation.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
(ID: {variation.id})
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 transition-transform group-open:rotate-180"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<div className="p-4 pt-0 space-y-3 border-t">
|
||||
<LinkRow
|
||||
label="Add to Cart"
|
||||
link={generateLink(variation.id, 'cart')}
|
||||
/>
|
||||
|
||||
<LinkRow
|
||||
label="Direct to Checkout"
|
||||
link={generateLink(variation.id, 'checkout')}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* URL Parameters Reference */}
|
||||
<div className="mt-6 p-4 bg-muted rounded-lg">
|
||||
<h4 className="font-medium text-sm mb-2">URL Parameters Reference</h4>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div><code className="bg-background px-1 py-0.5 rounded">add-to-cart</code> - Product ID (required)</div>
|
||||
<div><code className="bg-background px-1 py-0.5 rounded">variation_id</code> - Variation ID (for variable products)</div>
|
||||
<div><code className="bg-background px-1 py-0.5 rounded">quantity</code> - Quantity (default: 1)</div>
|
||||
<div><code className="bg-background px-1 py-0.5 rounded">redirect</code> - Destination: <code>cart</code> or <code>checkout</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ type Props = {
|
||||
className?: string;
|
||||
formRef?: React.RefObject<HTMLFormElement>;
|
||||
hideSubmitButton?: boolean;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export function ProductFormTabbed({
|
||||
@@ -50,6 +51,7 @@ export function ProductFormTabbed({
|
||||
className,
|
||||
formRef,
|
||||
hideSubmitButton = false,
|
||||
productId,
|
||||
}: Props) {
|
||||
// Form state
|
||||
const [name, setName] = useState(initial?.name || '');
|
||||
@@ -225,6 +227,7 @@ export function ProductFormTabbed({
|
||||
variations={variations}
|
||||
setVariations={setVariations}
|
||||
regularPrice={regularPrice}
|
||||
productId={productId}
|
||||
/>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Plus, X, Layers, Image as ImageIcon } from 'lucide-react';
|
||||
import { Plus, X, Layers, Image as ImageIcon, Copy, Check, ExternalLink } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStoreCurrency } from '@/lib/currency';
|
||||
import { openWPMediaImage } from '@/lib/wp-media';
|
||||
@@ -30,6 +30,7 @@ type VariationsTabProps = {
|
||||
variations: ProductVariant[];
|
||||
setVariations: (value: ProductVariant[]) => void;
|
||||
regularPrice: string;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export function VariationsTab({
|
||||
@@ -38,8 +39,33 @@ export function VariationsTab({
|
||||
variations,
|
||||
setVariations,
|
||||
regularPrice,
|
||||
productId,
|
||||
}: VariationsTabProps) {
|
||||
const store = getStoreCurrency();
|
||||
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
||||
|
||||
const siteUrl = window.location.origin;
|
||||
const spaPagePath = '/store';
|
||||
|
||||
const generateLink = (variationId: number, redirect: 'cart' | 'checkout' = 'cart') => {
|
||||
if (!productId) return '';
|
||||
const params = new URLSearchParams();
|
||||
params.set('add-to-cart', productId.toString());
|
||||
params.set('variation_id', variationId.toString());
|
||||
params.set('redirect', redirect);
|
||||
return `${siteUrl}${spaPagePath}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (link: string, label: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(link);
|
||||
setCopiedLink(link);
|
||||
toast.success(`${label} link copied!`);
|
||||
setTimeout(() => setCopiedLink(null), 2000);
|
||||
} catch (err) {
|
||||
toast.error('Failed to copy link');
|
||||
}
|
||||
};
|
||||
|
||||
const addAttribute = () => {
|
||||
setAttributes([...attributes, { name: '', options: [], variation: false }]);
|
||||
@@ -305,6 +331,45 @@ export function VariationsTab({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Direct Cart Links */}
|
||||
{productId && variation.id && (
|
||||
<div className="mt-4 pt-4 border-t space-y-2">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
{__('Direct-to-Cart Links')}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(generateLink(variation.id!, 'cart'), 'Cart')}
|
||||
className="flex-1"
|
||||
>
|
||||
{copiedLink === generateLink(variation.id!, 'cart') ? (
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{__('Copy Cart Link')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(generateLink(variation.id!, 'checkout'), 'Checkout')}
|
||||
className="flex-1"
|
||||
>
|
||||
{copiedLink === generateLink(variation.id!, 'checkout') ? (
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{__('Copy Checkout Link')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user