import { useState } from "react";
const initialData = [
{
id: 1,
date: "2026-04-12",
time: "10:00",
propertyAddress: "14 Maple Street, Austin, TX",
propertyType: "House",
bedrooms: 3,
bathrooms: 2,
price: "$450,000",
agent: "Sarah Johnson",
agentPhone: "512-555-0101",
status: "Confirmed",
rating: "4",
notes: "Corner lot, large backyard",
},
{
id: 2,
date: "2026-04-13",
time: "14:00",
propertyAddress: "88 Riverside Ave, Austin, TX",
propertyType: "Condo",
bedrooms: 2,
bathrooms: 1,
price: "$310,000",
agent: "Mark Thompson",
agentPhone: "512-555-0188",
status: "Pending",
rating: "3",
notes: "Near subway, parking included",
},
{
id: 3,
date: "2026-04-15",
time: "11:30",
propertyAddress: "5 Elmwood Blvd, Round Rock, TX",
propertyType: "Townhouse",
bedrooms: 4,
bathrooms: 3,
price: "$520,000",
agent: "Lisa Park",
agentPhone: "512-555-0234",
status: "Cancelled",
rating: "5",
notes: "New build, open plan kitchen",
},
];
const emptyRow = {
date: "",
time: "",
propertyAddress: "",
propertyType: "House",
bedrooms: "",
bathrooms: "",
price: "",
agent: "",
agentPhone: "",
status: "Pending",
rating: "",
notes: "",
};
const propertyTypes = ["House", "Condo", "Townhouse", "Apartment", "Studio", "Other"];
const statusOptions = ["Confirmed", "Pending", "Completed", "Cancelled"];
const ratingOptions = ["1", "2", "3", "4", "5"];
const statusColors = {
Confirmed: "bg-blue-100 text-blue-700",
Pending: "bg-yellow-100 text-yellow-700",
Completed: "bg-green-100 text-green-700",
Cancelled: "bg-red-100 text-red-700",
};
const ratingStars = (r) => "★".repeat(Number(r)) + "☆".repeat(5 - Number(r));
export default function App() {
const [rows, setRows] = useState(initialData);
const [editing, setEditing] = useState(null); // { rowId, field }
const [addingRow, setAddingRow] = useState(false);
const [newRow, setNewRow] = useState({ ...emptyRow });
const [filterStatus, setFilterStatus] = useState("All");
const [nextId, setNextId] = useState(4);
const filteredRows = filterStatus === "All" ? rows : rows.filter(r => r.status === filterStatus);
const updateCell = (id, field, value) => {
setRows(rows.map(r => r.id === id ? { ...r, [field]: value } : r));
setEditing(null);
};
const deleteRow = (id) => setRows(rows.filter(r => r.id !== id));
const addRow = () => {
setRows([...rows, { ...newRow, id: nextId }]);
setNextId(nextId + 1);
setNewRow({ ...emptyRow });
setAddingRow(false);
};
const exportCSV = () => {
const headers = ["Date","Time","Property Address","Type","Bed","Bath","Price","Agent","Agent Phone","Status","Rating","Notes"];
const csvRows = [headers.join(","), ...rows.map(r =>
[r.date, r.time, `"${r.propertyAddress}"`, r.propertyType, r.bedrooms, r.bathrooms,
`"${r.price}"`, `"${r.agent}"`, r.agentPhone, r.status, r.rating, `"${r.notes}"`].join(",")
)];
const blob = new Blob([csvRows.join("\n")], { type: "text/csv" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "property_viewing_schedule.csv";
a.click();
};
const EditableCell = ({ row, field, type = "text", options }) => {
const isEditing = editing?.rowId === row.id && editing?.field === field;
const val = row[field];
if (isEditing) {
if (options) return (
<select
autoFocus
className="w-full border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
defaultValue={val}
onChange={e => updateCell(row.id, field, e.target.value)}
onBlur={e => updateCell(row.id, field, e.target.value)}
>
{options.map(o => <option key={o}>{o}</option>)}
</select>
);
return (
<input
autoFocus
type={type}
className="w-full border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
defaultValue={val}
onBlur={e => updateCell(row.id, field, e.target.value)}
onKeyDown={e => e.key === "Enter" && updateCell(row.id, field, e.target.value)}
/>
);
}
if (field === "status") return (
<span
className={`px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer ${statusColors[val] || "bg-gray-100 text-gray-600"}`}
onClick={() => setEditing({ rowId: row.id, field })}
>{val}</span>
);
if (field === "rating") return (
<span className="cursor-pointer text-amber-400 text-xs" title={`${val}/5`}
onClick={() => setEditing({ rowId: row.id, field })}>
{val ? ratingStars(val) : <span className="text-gray-300">☆☆☆☆☆</span>}
</span>
);
return (
<span
className="cursor-pointer hover:bg-blue-50 rounded px-1 py-0.5 block truncate max-w-[160px]"
title={val}
onClick={() => setEditing({ rowId: row.id, field })}
>
{val || <span className="text-gray-300 italic text-xs">—</span>}
</span>
);
};
const cols = [
{ label: "Date", field: "date", type: "date", w: "w-28" },
{ label: "Time", field: "time", type: "time", w: "w-20" },
{ label: "Property Address", field: "propertyAddress", w: "w-52" },
{ label: "Type", field: "propertyType", options: propertyTypes, w: "w-24" },
{ label: "Bed", field: "bedrooms", type: "number", w: "w-14" },
{ label: "Bath", field: "bathrooms", type: "number", w: "w-14" },
{ label: "Price", field: "price", w: "w-28" },
{ label: "Agent", field: "agent", w: "w-32" },
{ label: "Agent Phone", field: "agentPhone", w: "w-28" },
{ label: "Status", field: "status", options: statusOptions, w: "w-28" },
{ label: "Rating", field: "rating", options: ratingOptions, w: "w-28" },
{ label: "Notes", field: "notes", w: "w-44" },
];
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-full mx-auto">
{/* Header */}
<div className="mb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-gray-800">🏠 Property Viewing Schedule</h1>
<p className="text-sm text-gray-500 mt-0.5">Click any cell to edit • {rows.length} properties tracked</p>
</div>
<div className="flex gap-2 flex-wrap">
<select
className="border border-gray-200 rounded-lg px-3 py-1.5 text-sm bg-white shadow-sm"
value={filterStatus}
onChange={e => setFilterStatus(e.target.value)}
>
<option value="All">All Statuses</option>
{statusOptions.map(s => <option key={s}>{s}</option>)}
</select>
<button
onClick={exportCSV}
className="bg-white border border-gray-200 text-gray-700 px-3 py-1.5 rounded-lg text-sm shadow-sm hover:bg-gray-50 flex items-center gap-1"
>⬇ Export CSV</button>
<button
onClick={() => setAddingRow(true)}
className="bg-blue-600 text-white px-4 py-1.5 rounded-lg text-sm shadow-sm hover:bg-blue-700 flex items-center gap-1"
>+ Add Viewing</button>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
{statusOptions.map(s => (
<div key={s} className={`rounded-xl p-3 ${statusColors[s]} bg-opacity-60`}>
<div className="text-2xl font-bold">{rows.filter(r => r.status === s).length}</div>
<div className="text-xs font-medium mt-0.5">{s}</div>
</div>
))}
</div>
{/* Table */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50 border-b border-gray-100">
{cols.map(c => (
<th key={c.field} className={`text-left px-3 py-2.5 font-semibold text-gray-600 text-xs uppercase tracking-wide ${c.w}`}>
{c.label}
</th>
))}
<th className="px-3 py-2.5 text-xs uppercase tracking-wide text-gray-400 w-12"></th>
</tr>
</thead>
<tbody>
{filteredRows.map((row, i) => (
<tr key={row.id} className={`border-b border-gray-50 hover:bg-gray-50 transition-colors ${i % 2 === 0 ? "" : "bg-gray-50/40"}`}>
{cols.map(c => (
<td key={c.field} className="px-3 py-2 text-gray-700">
<EditableCell row={row} field={c.field} type={c.type} options={c.options} />
</td>
))}
<td className="px-3 py-2 text-center">
<button onClick={() => deleteRow(row.id)} className="text-gray-300 hover:text-red-400 text-lg leading-none" title="Delete">×</button>
</td>
</tr>
))}
{filteredRows.length === 0 && (
<tr><td colSpan={cols.length + 1} className="text-center text-gray-400 py-10 italic">No viewings found.</td></tr>
)}
{/* Add Row */}
{addingRow && (
<tr className="bg-blue-50 border-t-2 border-blue-200">
{cols.map(c => (
<td key={c.field} className="px-2 py-2">
{c.options ? (
<select
className="w-full border border-blue-300 rounded px-1 py-1 text-xs bg-white"
value={newRow[c.field]}
onChange={e => setNewRow({ ...newRow, [c.field]: e.target.value })}
>
{c.options.map(o => <option key={o}>{o}</option>)}
</select>
) : (
<input
type={c.type || "text"}
className="w-full border border-blue-300 rounded px-1 py-1 text-xs bg-white"
placeholder={c.label}
value={newRow[c.field]}
onChange={e => setNewRow({ ...newRow, [c.field]: e.target.value })}
/>
)}
</td>
))}
<td className="px-2 py-2 text-center">
<button onClick={() => setAddingRow(false)} className="text-gray-400 hover:text-red-400 text-lg leading-none">×</button>
</td>
</tr>
)}
</tbody>
</table>
{addingRow && (
<div className="flex justify-end gap-2 px-4 py-3 bg-blue-50 border-t border-blue-100">
<button onClick={() => setAddingRow(false)} className="px-4 py-1.5 rounded-lg border border-gray-200 text-sm bg-white hover:bg-gray-50">Cancel</button>
<button onClick={addRow} className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">Save Row</button>
</div>
)}
</div>
<p className="text-xs text-gray-400 mt-3 text-center">Click any cell to edit inline • Export to CSV to open in Excel or Google Sheets</p>
</div>
</div>
);
}