<?php
/*
* Plugin Name: Custom Eyeglass Prescription & Virtual Try-On
* Description: All-in-one solution: React-based prescription form + AI Virtual Try-On + Dynamic Lens Pricing for WooCommerce.
* Version: 3.0 (Lens Packages)
*/
// ---------------------------------------------------------
// 1. ADMIN SETTINGS (Enable per product)
// ---------------------------------------------------------
add_action( 'woocommerce_product_options_general_product_data', 'eyewear_add_product_option' );
function eyewear_add_product_option() {
echo '<div class="options_group">';
woocommerce_wp_checkbox( array(
'id' => '_enable_eyewear_prescription',
'label' => __( 'Enable Prescription Form', 'woocommerce' ),
'description' => __( 'Check this box to show the Eyewear Prescription, Try-On & Lens Packages for this product.', 'woocommerce' ),
'desc_tip' => true,
) );
echo '</div>';
}
add_action( 'woocommerce_process_product_meta', 'eyewear_save_product_option' );
function eyewear_save_product_option( $post_id ) {
$checkbox = isset( $_POST['_enable_eyewear_prescription'] ) ? 'yes' : 'no';
update_post_meta( $post_id, '_enable_eyewear_prescription', $checkbox );
}
// ---------------------------------------------------------
// 2. FRONTEND: Form Rendering
// ---------------------------------------------------------
add_action('woocommerce_before_add_to_cart_button', 'render_eyewear_interface');
function render_eyewear_interface() {
global $product;
$is_enabled = get_post_meta( $product->get_id(), '_enable_eyewear_prescription', true );
if ( $is_enabled !== 'yes' ) return;
?>
<!-- Dependencies -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<style>
#eyewear-app-root { margin-top: 20px; margin-bottom: 20px; clear: both; font-family: 'Inter', system-ui, sans-serif; width: 100%; }
.glass-panel { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid rgba(226, 232, 240, 0.8); }
/* Custom Card Styles matching Screenshot */
.lens-card { transition: all 0.2s ease; border: 2px solid transparent; }
.lens-card:hover { transform: translateY(-2px); shadow: lg; }
.lens-card.selected { border-color: #2563eb; transform: scale(1.02); box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.2); }
.header-discover { background-color: #94a3b8; color: white; } /* Grey */
.header-inspire-blue { background-color: #22d3ee; color: white; } /* Cyan */
.header-inspire-green { background-color: #22c55e; color: white; } /* Green */
.header-blue { background-color: #64748b; color: white; } /* Slate Blueish */
.ai-scanner {
height: 2px;
width: 100%;
background: linear-gradient(90deg, transparent, #3b82f6, transparent);
position: absolute;
top: 0; left: 0;
animation: scan 2s infinite linear;
}
@keyframes scan { 0% { top: 0%; } 50% { top: 100%; } 100% { top: 0%; } }
</style>
<div id="eyewear-app-root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
// --- DATA: Lens Packages (Based on your screenshots) ---
const LENS_PACKAGES = {
frame_only: [],
plano: [
{ id: 'plano_std', name: 'Standard Plano', type: 'Discover', headerClass: 'bg-slate-400', desc: 'No prescription, fashion use', price: 0 }
],
// Single Vision & Bifocal share similar pricing in screenshot 1
single_vision: [
{ id: 'poly', name: 'Polycarbonate', type: 'Discover', headerClass: 'bg-slate-500', desc: 'Stock lens\nQuality Clear AR', price: 0 },
{ id: 'cr39', name: 'CR-39', type: 'Discover', headerClass: 'bg-gray-400', desc: 'Stock lens\nQuality Clear AR', price: 0 },
{ id: 'inspire160', name: '1.60', type: 'Inspire', headerClass: 'bg-cyan-400', desc: 'Digital Freeform\nBlue Light Filter', price: 100 },
{ id: 'inspire167', name: '1.67', type: 'Inspire', headerClass: 'bg-cyan-500', desc: 'Digital Freeform\nHigh Index', price: 100 },
],
bifocal: [
{ id: 'bi_poly', name: 'Polycarbonate', type: 'Discover', headerClass: 'bg-slate-500', desc: 'Stock lens\nQuality Clear AR', price: 0 },
{ id: 'bi_cr39', name: 'CR-39', type: 'Discover', headerClass: 'bg-gray-400', desc: 'Stock lens\nQuality Clear AR', price: 0 },
{ id: 'bi_160', name: '1.60', type: 'Inspire', headerClass: 'bg-cyan-400', desc: 'Digital Freeform\nBlue Light Filter', price: 100 },
],
// Progressive has higher pricing as per screenshot 2
progressive: [
{ id: 'prog_poly', name: 'Polycarbonate', type: 'Discover', headerClass: 'bg-slate-500', desc: 'Digital Freeform\nQuality Clear AR', price: 100 },
{ id: 'prog_cr39', name: 'CR-39', type: 'Discover', headerClass: 'bg-gray-400', desc: 'Digital Freeform\nQuality Clear AR', price: 100 },
{ id: 'prog_160', name: '1.60', type: 'Inspire', headerClass: 'bg-cyan-400', desc: 'Digital Freeform\nPremium Clear AR', price: 200 },
{ id: 'prog_167', name: '1.67', type: 'Inspire', headerClass: 'bg-cyan-500', desc: 'Digital Freeform\nBlue Light Filter', price: 200 },
{ id: 'prog_trivex', name: 'Trivex', type: 'Inspire', headerClass: 'bg-green-500', desc: 'Digital Freeform\nImpact Resistant', price: 100 }, // Estimated
]
};
const SPHERE_OPTIONS = Array.from({length: 73}, (_, i) => { const v = (-10 + i * 0.25).toFixed(2); return v > 0 ? `+${v}` : v; }).sort((a,b)=>parseFloat(b)-parseFloat(a));
const CYL_OPTIONS = Array.from({length: 49}, (_, i) => { const v = (-6 + i * 0.25).toFixed(2); return v > 0 ? `+${v}` : v; }).sort((a,b)=>parseFloat(b)-parseFloat(a));
const AXIS_OPTIONS = Array.from({length: 181}, (_, i) => i.toString().padStart(3, '0'));
const ADD_OPTIONS = Array.from({length: 14}, (_, i) => `+${(0.75 + (i * 0.25)).toFixed(2)}`);
// --- COMPONENTS ---
const LensCard = ({ pkg, selected, onClick }) => (
<div
onClick={() => onClick(pkg)}
className={`lens-card cursor-pointer rounded-lg overflow-hidden bg-white shadow-sm border border-gray-200 flex flex-col text-center h-full ${selected ? 'selected ring-2 ring-blue-500' : ''}`}
>
<div className={`${pkg.headerClass} py-2 px-1`}>
<div className="text-xs uppercase tracking-widest font-bold opacity-90">{pkg.type}</div>
<div className="text-lg font-bold">{pkg.name}</div>
</div>
<div className="p-3 flex-grow flex flex-col justify-center gap-2">
<p className="text-xs text-gray-600 whitespace-pre-line font-medium leading-tight">{pkg.desc}</p>
<div className="mt-2 text-sm font-bold text-gray-800">
{pkg.price > 0 ? `+$${pkg.price}` : '+$0 (Included)'}
</div>
<div className="mt-1">
<div className={`w-4 h-4 rounded-full border mx-auto ${selected ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'}`}>
{selected && <i className="fa-solid fa-check text-white text-[10px] block"></i>}
</div>
</div>
</div>
</div>
);
const SelectField = ({ label, value, options, onChange, name }) => (
<div className="relative">
<label className="block md:hidden text-[10px] font-bold text-gray-400 mb-1 uppercase">{label}</label>
<select name={name} value={value} onChange={onChange} className="w-full bg-white border border-gray-200 text-gray-700 text-sm rounded px-2 py-2 focus:ring-blue-500 focus:border-blue-500">
<option value="" disabled>Select</option>
{options.map((o, i) => <option key={i} value={o}>{o}</option>)}
</select>
</div>
);
// --- MAIN APP ---
const App = () => {
const [tab, setTab] = useState('prescription'); // prescription, tryon
const [rxType, setRxType] = useState('single_vision'); // frame_only, plano, single_vision, bifocal, progressive
const [selectedPkg, setSelectedPkg] = useState(null);
const [pdType, setPdType] = useState('single_pd');
const [formData, setFormData] = useState({
od_sph: '0.00', od_cyl: '0.00', od_axis: '', od_add: '',
os_sph: '0.00', os_cyl: '0.00', os_axis: '', os_add: '',
pd_single: '63', pd_right: '31.5', pd_left: '31.5', memo: ''
});
// Reset package when type changes
useEffect(() => {
const pkgs = LENS_PACKAGES[rxType] || [];
if (pkgs.length > 0) {
setSelectedPkg(pkgs[0]); // Default to first option
} else {
setSelectedPkg(null);
}
}, [rxType]);
const handleChange = (e) => setFormData({...formData, [e.target.name]: e.target.value});
const needsRxData = ['single_vision', 'bifocal', 'progressive'].includes(rxType);
const needsAdd = ['bifocal', 'progressive'].includes(rxType);
return (
<div className="bg-slate-50 rounded-xl shadow-lg border border-slate-200 overflow-hidden text-left mb-6">
{/* Hidden Inputs to send data to PHP */}
<input type="hidden" name="custom_rx_type" value={rxType} />
<input type="hidden" name="custom_lens_package" value={selectedPkg ? selectedPkg.name : ''} />
<input type="hidden" name="custom_lens_price" value={selectedPkg ? selectedPkg.price : 0} />
<input type="hidden" name="custom_pd_type" value={pdType} />
{needsRxData && Object.keys(formData).map(key => (
<input key={key} type="hidden" name={`custom_rx_${key}`} value={formData[key]} />
))}
{/* Header Tabs */}
<div className="flex bg-white border-b border-gray-200">
<button type="button" onClick={() => setTab('prescription')} className={`flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 ${tab === 'prescription' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500'}`}>
<i className="fa-solid fa-glasses"></i> Configuration
</button>
<button type="button" onClick={() => setTab('tryon')} className={`flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 ${tab === 'tryon' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500'}`}>
<i className="fa-solid fa-camera"></i> Virtual Try-On
</button>
</div>
<div className="p-4 md:p-6">
{tab === 'prescription' ? (
<div className="space-y-8 animate__animated animate__fadeIn">
{/* 1. Lens Type Selection (Radio Group Style) */}
<div>
<h3 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
<span className="bg-green-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">1</span>
Choose Prescription Type
</h3>
<div className="flex flex-wrap gap-4 text-sm">
{[
{id: 'frame_only', label: 'Frame only'},
{id: 'plano', label: 'Plano (Non-Rx)'},
{id: 'single_vision', label: 'Single vision lenses'},
{id: 'bifocal', label: 'Bi-Focal'},
{id: 'progressive', label: 'Progressive lenses'}
].map(opt => (
<label key={opt.id} className="flex items-center gap-2 cursor-pointer group">
<div className={`w-4 h-4 rounded-full border flex items-center justify-center ${rxType === opt.id ? 'border-blue-500' : 'border-gray-400 group-hover:border-blue-400'}`}>
{rxType === opt.id && <div className="w-2 h-2 rounded-full bg-blue-500"></div>}
</div>
<span className={`${rxType === opt.id ? 'font-bold text-gray-900' : 'text-gray-600 group-hover:text-gray-900'}`}>{opt.label}</span>
<input type="radio" className="hidden" name="rx_type_select" checked={rxType === opt.id} onChange={() => setRxType(opt.id)} />
</label>
))}
</div>
</div>
{/* 2. Lens Material Packages (The Cards) */}
{LENS_PACKAGES[rxType] && LENS_PACKAGES[rxType].length > 0 && (
<div className="animate__animated animate__fadeInUp">
<h3 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
<span className="bg-green-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">2</span>
Choose Package by Lens Material
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
{LENS_PACKAGES[rxType].map((pkg) => (
<LensCard
key={pkg.id}
pkg={pkg}
selected={selectedPkg && selectedPkg.id === pkg.id}
onClick={setSelectedPkg}
/>
))}
</div>
</div>
)}
{/* 3. Prescription Values (Only if needed) */}
{needsRxData && (
<div className="bg-slate-100 rounded-lg p-4 animate__animated animate__fadeIn">
<h3 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
<span className="bg-green-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">3</span>
Enter Prescription
</h3>
<div className="grid grid-cols-[40px_1fr] gap-3">
{/* Header */}
<div></div>
<div className={`grid ${needsAdd ? 'grid-cols-4' : 'grid-cols-3'} gap-2 text-center text-[10px] font-bold text-gray-500`}>
<div>SPH</div><div>CYL</div><div>AXIS</div>{needsAdd && <div>ADD</div>}
</div>
{/* OD */}
<div className="flex items-center justify-center font-bold text-blue-600 text-sm">OD</div>
<div className={`grid ${needsAdd ? 'grid-cols-4' : 'grid-cols-3'} gap-2`}>
<SelectField name="od_sph" value={formData.od_sph} options={SPHERE_OPTIONS} onChange={handleChange} label="SPH" />
<SelectField name="od_cyl" value={formData.od_cyl} options={CYL_OPTIONS} onChange={handleChange} label="CYL" />
<SelectField name="od_axis" value={formData.od_axis} options={AXIS_OPTIONS} onChange={handleChange} label="AXIS" />
{needsAdd && <SelectField name="od_add" value={formData.od_add} options={ADD_OPTIONS} onChange={handleChange} label="ADD" />}
</div>
{/* OS */}
<div className="flex items-center justify-center font-bold text-blue-600 text-sm">OS</div>
<div className={`grid ${needsAdd ? 'grid-cols-4' : 'grid-cols-3'} gap-2`}>
<SelectField name="os_sph" value={formData.os_sph} options={SPHERE_OPTIONS} onChange={handleChange} label="SPH" />
<SelectField name="os_cyl" value={formData.os_cyl} options={CYL_OPTIONS} onChange={handleChange} label="CYL" />
<SelectField name="os_axis" value={formData.os_axis} options={AXIS_OPTIONS} onChange={handleChange} label="AXIS" />
{needsAdd && <SelectField name="os_add" value={formData.os_add} options={ADD_OPTIONS} onChange={handleChange} label="ADD" />}
</div>
</div>
{/* PD */}
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between mb-2">
<label className="text-xs font-bold text-gray-600">Pupillary Distance (PD)</label>
<div className="flex bg-white rounded border border-gray-200">
<button type="button" onClick={()=>setPdType('single_pd')} className={`px-2 py-1 text-[10px] font-bold ${pdType==='single_pd'?'bg-blue-50 text-blue-600':'text-gray-500'}`}>Single</button>
<button type="button" onClick={()=>setPdType('dual_pd')} className={`px-2 py-1 text-[10px] font-bold ${pdType==='dual_pd'?'bg-blue-50 text-blue-600':'text-gray-500'}`}>Dual</button>
</div>
</div>
<div className="flex gap-4">
{pdType === 'single_pd' ? (
<div className="w-1/3"><SelectField name="pd_single" value={formData.pd_single} options={Array.from({length:31},(_,i)=>(50+i).toString())} onChange={handleChange} /></div>
) : (
<>
<div className="w-1/3"><SelectField name="pd_right" value={formData.pd_right} options={Array.from({length:16},(_,i)=>(25+i).toString())} onChange={handleChange} /></div>
<div className="w-1/3"><SelectField name="pd_left" value={formData.pd_left} options={Array.from({length:16},(_,i)=>(25+i).toString())} onChange={handleChange} /></div>
</>
)}
</div>
</div>
</div>
)}
</div>
) : (
// Simple Try-On Placeholder
<div className="text-center py-10">
<i className="fa-solid fa-camera text-4xl text-gray-300 mb-3"></i>
<p className="text-gray-500 text-sm">Upload a photo to try on frames.</p>
<button className="mt-4 bg-slate-800 text-white px-4 py-2 rounded-lg text-sm">Upload Photo</button>
</div>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('eyewear-app-root'));
root.render(<App />);
</script>
<?php
}
// ---------------------------------------------------------
// 3. BACKEND: Save Data & Add Price
// ---------------------------------------------------------
// A. Save Custom Data to Cart Item
add_filter('woocommerce_add_cart_item_data', 'save_lens_data_to_cart', 10, 2);
function save_lens_data_to_cart($cart_item_data, $product_id) {
if (isset($_POST['custom_rx_type'])) {
$cart_item_data['rx_data'] = array(
'Type' => sanitize_text_field($_POST['custom_rx_type']),
);
// Save Lens Package & Price
if (!empty($_POST['custom_lens_package'])) {
$cart_item_data['rx_data']['Lens Package'] = sanitize_text_field($_POST['custom_lens_package']);
}
if (isset($_POST['custom_lens_price'])) {
$price = floatval($_POST['custom_lens_price']);
$cart_item_data['rx_data']['Lens Price'] = $price;
}
// Save Rx Values
$fields = ['od_sph', 'od_cyl', 'od_axis', 'od_add', 'os_sph', 'os_cyl', 'os_axis', 'os_add', 'pd_single', 'pd_right', 'pd_left'];
foreach ($fields as $field) {
if (isset($_POST['custom_rx_' . $field])) {
$cart_item_data['rx_data'][$field] = sanitize_text_field($_POST['custom_rx_' . $field]);
}
}
$cart_item_data['unique_key'] = md5(microtime().rand());
}
return $cart_item_data;
}
// B. Calculate Dynamic Price (Add Lens Price to Product Price)
add_action('woocommerce_before_calculate_totals', 'add_lens_price_to_total', 20, 1);
function add_lens_price_to_total($cart) {
if (is_admin() && !defined('DOING_AJAX')) return;
foreach ($cart->get_cart() as $cart_item) {
if (isset($cart_item['rx_data']['Lens Price']) && floatval($cart_item['rx_data']['Lens Price']) > 0) {
$extra_cost = floatval($cart_item['rx_data']['Lens Price']);
// Get original price and add extra cost
$org_price = floatval($cart_item['data']->get_price());
$cart_item['data']->set_price($org_price + $extra_cost);
}
}
}
// ---------------------------------------------------------
// 4. DISPLAY: Cart & Order
// ---------------------------------------------------------
add_filter('woocommerce_get_item_data', 'display_lens_data_in_cart', 10, 2);
function display_lens_data_in_cart($item_data, $cart_item) {
if (isset($cart_item['rx_data'])) {
$rx = $cart_item['rx_data'];
$item_data[] = array('key' => 'Rx Type', 'value' => ucfirst(str_replace('_', ' ', $rx['Type'])));
if (!empty($rx['Lens Package'])) {
$price_display = isset($rx['Lens Price']) && $rx['Lens Price'] > 0 ? ' (+$' . $rx['Lens Price'] . ')' : '';
$item_data[] = array('key' => 'Lens Package', 'value' => $rx['Lens Package'] . $price_display);
}
if ($rx['Type'] !== 'frame_only' && $rx['Type'] !== 'plano') {
// Simplified display for Rx
if(!empty($rx['od_sph'])) $item_data[] = array('key' => 'OD', 'value' => "{$rx['od_sph']} / {$rx['od_cyl']} x {$rx['od_axis']}");
if(!empty($rx['os_sph'])) $item_data[] = array('key' => 'OS', 'value' => "{$rx['os_sph']} / {$rx['os_cyl']} x {$rx['os_axis']}");
}
}
return $item_data;
}
add_action('woocommerce_checkout_create_order_line_item', 'add_lens_data_to_order', 10, 4);
function add_lens_data_to_order($item, $cart_item_key, $values, $order) {
if (isset($values['rx_data'])) {
$rx = $values['rx_data'];
$item->add_meta_data('Rx Type', ucfirst(str_replace('_', ' ', $rx['Type'])));
if (!empty($rx['Lens Package'])) {
$price_display = isset($rx['Lens Price']) && $rx['Lens Price'] > 0 ? ' (+$' . $rx['Lens Price'] . ')' : '';
$item->add_meta_data('Lens Package', $rx['Lens Package'] . $price_display);
}
if ($rx['Type'] !== 'frame_only' && $rx['Type'] !== 'plano') {
$item->add_meta_data('Right Eye (OD)', "{$rx['od_sph']} / {$rx['od_cyl']} x {$rx['od_axis']}" . (!empty($rx['od_add'])?" Add: {$rx['od_add']}":""));
$item->add_meta_data('Left Eye (OS)', "{$rx['os_sph']} / {$rx['os_cyl']} x {$rx['os_axis']}" . (!empty($rx['os_add'])?" Add: {$rx['os_add']}":""));
$item->add_meta_data('PD', isset($rx['pd_single']) ? $rx['pd_single'] : "R: {$rx['pd_right']} / L: {$rx['pd_left']}");
}
}
}
?>
demo new
14
Feb