Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import type { Product } from "@/lib/data/products";
|
||
|
||
export type StorageNote = {
|
||
icon: "thermometer-outline" | "sunny-outline" | "happy-outline" | "water-outline";
|
||
title: string;
|
||
body: string;
|
||
};
|
||
|
||
export type DrugFacts = {
|
||
/** Short therapeutic label shown above the product name. */
|
||
useCase: string;
|
||
/** Dose form: Tablet, Syrup, Cream, Device… */
|
||
form: string;
|
||
genericName: string;
|
||
brandNames: string;
|
||
drugClass: string;
|
||
/** Selectable strengths on the detail page. Empty for non-dosed products. */
|
||
dosages: string[];
|
||
sideEffects: { mild: string[]; severe: string[] };
|
||
storage: StorageNote[];
|
||
directions: { title: string; body: string }[];
|
||
stockCount: number;
|
||
/** Pharmacist guidance shown in the safety callout. */
|
||
advice: string;
|
||
};
|
||
|
||
const CATEGORY_USE_CASE: Record<string, string> = {
|
||
medicines: "General Medicine",
|
||
vitamins: "Supplements",
|
||
"personal-care": "Personal Care",
|
||
diabetic: "Diabetes Care",
|
||
devices: "Medical Device",
|
||
baby: "Baby Care",
|
||
"first-aid": "First Aid",
|
||
wellness: "Sexual Wellness",
|
||
};
|
||
|
||
const CATEGORY_FORM: Record<string, string> = {
|
||
medicines: "Tablet",
|
||
vitamins: "Tablet",
|
||
"personal-care": "Topical",
|
||
diabetic: "Tablet",
|
||
devices: "Device",
|
||
baby: "Topical",
|
||
"first-aid": "Supply",
|
||
wellness: "Tablet",
|
||
};
|
||
|
||
/** Per-product clinical overrides. Everything else falls back to category defaults. */
|
||
const OVERRIDES: Record<string, Partial<DrugFacts>> = {
|
||
"panadol-advance": {
|
||
useCase: "Pain & Fever",
|
||
genericName: "Paracetamol (Acetaminophen)",
|
||
brandNames: "Panadol, Tylenol, Calpol",
|
||
drugClass: "Analgesic / Antipyretic",
|
||
dosages: ["500mg", "650mg"],
|
||
sideEffects: {
|
||
mild: ["Nausea", "Sleepiness", "Stomach upset"],
|
||
severe: ["Liver damage", "Rash", "Breathing issues"],
|
||
},
|
||
advice: "Do not exceed 4g of paracetamol in 24 hours across all products.",
|
||
},
|
||
paracetamol: {
|
||
useCase: "Pain & Fever",
|
||
genericName: "Paracetamol",
|
||
brandNames: "Panadol, Tylenol",
|
||
drugClass: "Analgesic / Antipyretic",
|
||
dosages: ["500mg"],
|
||
advice: "Avoid alcohol while taking this medicine.",
|
||
},
|
||
"paracetamol-tylenol": {
|
||
useCase: "Pain & Fever",
|
||
genericName: "Acetaminophen",
|
||
brandNames: "Tylenol, Panadol",
|
||
drugClass: "Analgesic",
|
||
dosages: ["500mg", "650mg"],
|
||
},
|
||
"paracetamol-caffeine": {
|
||
useCase: "Headache Relief",
|
||
genericName: "Paracetamol + Caffeine",
|
||
brandNames: "Panadol Extra",
|
||
drugClass: "Analgesic combination",
|
||
dosages: ["500mg / 65mg"],
|
||
advice: "Contains caffeine — limit other caffeinated drinks.",
|
||
},
|
||
snip: {
|
||
useCase: "Cold & Flu",
|
||
genericName: "Paracetamol + Pseudoephedrine + Chlorphenamine",
|
||
brandNames: "SNIP",
|
||
drugClass: "Decongestant combination",
|
||
sideEffects: {
|
||
mild: ["Drowsiness", "Dry mouth", "Dizziness"],
|
||
severe: ["Rapid heartbeat", "High blood pressure"],
|
||
},
|
||
advice: "May cause drowsiness. Do not drive until you know how it affects you.",
|
||
},
|
||
metformin: {
|
||
useCase: "Diabetes Care",
|
||
genericName: "Metformin hydrochloride",
|
||
brandNames: "Glucophage, Glumet",
|
||
drugClass: "Biguanide antidiabetic",
|
||
dosages: ["500mg", "850mg", "1000mg"],
|
||
sideEffects: {
|
||
mild: ["Nausea", "Diarrhoea", "Metallic taste"],
|
||
severe: ["Lactic acidosis", "Vitamin B12 deficiency"],
|
||
},
|
||
advice: "Take with meals to reduce stomach upset. Requires a valid prescription.",
|
||
},
|
||
ciprofloxacin: {
|
||
useCase: "Antibiotic",
|
||
genericName: "Ciprofloxacin",
|
||
brandNames: "Cipro, Ciprobay",
|
||
drugClass: "Fluoroquinolone antibiotic",
|
||
dosages: ["250mg", "500mg"],
|
||
sideEffects: {
|
||
mild: ["Nausea", "Headache", "Dizziness"],
|
||
severe: ["Tendon rupture", "Nerve damage"],
|
||
},
|
||
advice: "Finish the full course even if you feel better.",
|
||
},
|
||
azitro: {
|
||
useCase: "Antibiotic",
|
||
genericName: "Azithromycin",
|
||
brandNames: "Zithromax, Azitro",
|
||
drugClass: "Macrolide antibiotic",
|
||
dosages: ["250mg", "500mg"],
|
||
advice: "Usually taken once daily for 3 days. Complete the course as prescribed.",
|
||
},
|
||
diclofenac: {
|
||
useCase: "Pain & Inflammation",
|
||
genericName: "Diclofenac sodium",
|
||
brandNames: "Voltaren, Cataflam",
|
||
drugClass: "NSAID",
|
||
dosages: ["25mg", "50mg", "75mg"],
|
||
sideEffects: {
|
||
mild: ["Indigestion", "Headache", "Dizziness"],
|
||
severe: ["Stomach bleeding", "Kidney issues"],
|
||
},
|
||
advice: "Take after food. Avoid if you have a history of stomach ulcers.",
|
||
},
|
||
pantoprazole: {
|
||
useCase: "Acid Reflux",
|
||
genericName: "Pantoprazole sodium",
|
||
brandNames: "Protonix, Pantocid",
|
||
drugClass: "Proton pump inhibitor",
|
||
dosages: ["20mg", "40mg"],
|
||
advice: "Take 30–60 minutes before your first meal of the day.",
|
||
},
|
||
desloratidine: {
|
||
useCase: "Allergy Relief",
|
||
genericName: "Desloratadine",
|
||
brandNames: "Clarinex, Aerius",
|
||
drugClass: "Antihistamine",
|
||
dosages: ["5mg"],
|
||
advice: "Non-drowsy for most people, but avoid driving until you're sure.",
|
||
},
|
||
"nat-c-vitamin-c": {
|
||
useCase: "Immune Support",
|
||
genericName: "Ascorbic acid",
|
||
brandNames: "Nat C, Redoxon",
|
||
drugClass: "Vitamin supplement",
|
||
dosages: ["500mg", "1000mg"],
|
||
sideEffects: { mild: ["Stomach upset", "Heartburn"], severe: ["Kidney stones (high doses)"] },
|
||
},
|
||
gofen: {
|
||
useCase: "Pain & Inflammation",
|
||
genericName: "Ibuprofen",
|
||
brandNames: "Brufen, Advil, Gofen",
|
||
drugClass: "NSAID",
|
||
dosages: ["200mg", "400mg"],
|
||
advice: "Take with food or milk to protect your stomach.",
|
||
},
|
||
quemox: {
|
||
useCase: "Antibiotic",
|
||
genericName: "Amoxicillin",
|
||
brandNames: "Amoxil, Quemox",
|
||
drugClass: "Penicillin antibiotic",
|
||
dosages: ["250mg", "500mg"],
|
||
advice: "Tell your pharmacist about any penicillin allergy before use.",
|
||
},
|
||
sildenafil: {
|
||
useCase: "Sexual Wellness",
|
||
genericName: "Sildenafil citrate",
|
||
brandNames: "Viagra, Cialis (tadalafil alt.)",
|
||
drugClass: "PDE5 inhibitor",
|
||
dosages: ["25mg", "50mg", "100mg"],
|
||
advice: "Never combine with nitrate heart medicines. Prescription required.",
|
||
},
|
||
"ors-sachets": {
|
||
useCase: "Rehydration",
|
||
genericName: "Oral rehydration salts",
|
||
brandNames: "ORS, Hydralyte",
|
||
drugClass: "Electrolyte replacement",
|
||
advice: "Dissolve one sachet in 200ml of clean water. Use within 24 hours.",
|
||
},
|
||
"cough-syrup": {
|
||
useCase: "Cough Relief",
|
||
form: "Syrup",
|
||
genericName: "Dextromethorphan / Guaifenesin",
|
||
brandNames: "Robitussin, Benylin",
|
||
drugClass: "Antitussive / Expectorant",
|
||
dosages: ["100ml", "200ml"],
|
||
},
|
||
"lubricating-eye-drops": {
|
||
useCase: "Eye Care",
|
||
form: "Drops",
|
||
},
|
||
"eye-drops-lubricating": {
|
||
useCase: "Eye Care",
|
||
form: "Drops",
|
||
genericName: "Carboxymethylcellulose",
|
||
brandNames: "Refresh Tears, Optive",
|
||
drugClass: "Ocular lubricant",
|
||
},
|
||
"blood-pressure-monitor": {
|
||
useCase: "Monitoring",
|
||
form: "Device",
|
||
genericName: "Upper-arm oscillometric monitor",
|
||
brandNames: "HealthTech",
|
||
drugClass: "Class II medical device",
|
||
},
|
||
"glucose-meter-kit": {
|
||
useCase: "Diabetes Care",
|
||
form: "Device",
|
||
genericName: "Blood glucose monitoring system",
|
||
brandNames: "DiaCare",
|
||
drugClass: "Class II medical device",
|
||
},
|
||
};
|
||
|
||
const DEFAULT_SIDE_EFFECTS: DrugFacts["sideEffects"] = {
|
||
mild: ["Nausea", "Headache", "Mild dizziness"],
|
||
severe: ["Allergic reaction", "Difficulty breathing"],
|
||
};
|
||
|
||
const DEFAULT_STORAGE: StorageNote[] = [
|
||
{
|
||
icon: "thermometer-outline",
|
||
title: "Temperature",
|
||
body: "Store at room temperature, between 20°C and 25°C.",
|
||
},
|
||
{
|
||
icon: "sunny-outline",
|
||
title: "Light & moisture",
|
||
body: "Keep in a dry place, away from direct sunlight.",
|
||
},
|
||
{
|
||
icon: "happy-outline",
|
||
title: "Children",
|
||
body: "Keep out of reach of children to prevent accidental ingestion.",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_DIRECTIONS: DrugFacts["directions"] = [
|
||
{
|
||
title: "Check with your pharmacist",
|
||
body: "Confirm the dose is right for your age, weight and other medicines.",
|
||
},
|
||
{
|
||
title: "Take as directed",
|
||
body: "Follow the schedule on the label. Do not double up on a missed dose.",
|
||
},
|
||
{
|
||
title: "Take with water",
|
||
body: "Swallow whole with a full glass of water unless told otherwise.",
|
||
},
|
||
{
|
||
title: "Track your response",
|
||
body: "Stop and contact us if symptoms persist beyond 3 days.",
|
||
},
|
||
];
|
||
|
||
const DEVICE_DIRECTIONS: DrugFacts["directions"] = [
|
||
{ title: "Unbox and inspect", body: "Check all parts are present and undamaged." },
|
||
{ title: "Prepare", body: "Sit still for 5 minutes and rest your arm at heart level." },
|
||
{ title: "Measure", body: "Follow the on-screen prompts and stay quiet during the reading." },
|
||
{ title: "Log the result", body: "Record readings so your pharmacist can review trends." },
|
||
];
|
||
|
||
/** Deterministic pseudo-stock so the number is stable between renders. */
|
||
function stockFor(product: Product): number {
|
||
if (!product.inStock) return 0;
|
||
const seed = product.id
|
||
.split("")
|
||
.reduce((total, char) => total + char.charCodeAt(0), 0);
|
||
return 8 + (seed % 44);
|
||
}
|
||
|
||
export function getDrugFacts(product: Product): DrugFacts {
|
||
const override = OVERRIDES[product.slug] ?? {};
|
||
const isDevice = product.category === "devices" || product.category === "first-aid";
|
||
|
||
return {
|
||
useCase: override.useCase ?? CATEGORY_USE_CASE[product.category] ?? "Healthcare",
|
||
form: override.form ?? CATEGORY_FORM[product.category] ?? "Unit",
|
||
genericName: override.genericName ?? product.name,
|
||
brandNames: override.brandNames ?? product.brand,
|
||
drugClass: override.drugClass ?? CATEGORY_USE_CASE[product.category] ?? "Healthcare product",
|
||
dosages: override.dosages ?? [],
|
||
sideEffects:
|
||
override.sideEffects ??
|
||
(isDevice ? { mild: ["Skin irritation"], severe: [] } : DEFAULT_SIDE_EFFECTS),
|
||
storage: override.storage ?? DEFAULT_STORAGE,
|
||
directions: override.directions ?? (isDevice ? DEVICE_DIRECTIONS : DEFAULT_DIRECTIONS),
|
||
stockCount: override.stockCount ?? stockFor(product),
|
||
advice:
|
||
override.advice ??
|
||
(product.requiresPrescription
|
||
? "A valid prescription is required. Our pharmacist will verify it before dispatch."
|
||
: "Speak to our pharmacist if you are pregnant, breastfeeding or taking other medicines."),
|
||
};
|
||
}
|
||
|
||
/** Star distribution for the review summary, weighted by the product rating. */
|
||
export function getReviewBreakdown(product: Product) {
|
||
const weights = [0.62, 0.22, 0.09, 0.04, 0.03];
|
||
const shift = Math.max(0, Math.round((4.8 - product.rating) * 10)) / 100;
|
||
return [5, 4, 3, 2, 1].map((stars, index) => {
|
||
const weight = Math.max(0.01, weights[index] - (index === 0 ? shift : -shift / 4));
|
||
return { stars, count: Math.max(1, Math.round(product.reviews * weight)) };
|
||
});
|
||
}
|