Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9ff53bb
Change categories to use id instead of name
AMTuttle02 Jan 29, 2026
15608f3
Include subcategory column in coupon logic and change to use ids inst…
AMTuttle02 Jan 29, 2026
feee058
Fix coupon code logic
AMTuttle02 Jan 29, 2026
26ac478
Merge pull request #308 from AMTuttle02/feat296_FixCategoryColumn
AMTuttle02 Jan 29, 2026
06157b9
Add page numbers to products pages for easier routing
AMTuttle02 Jan 29, 2026
6c1b124
Improve filtering and sorting options
AMTuttle02 Jan 30, 2026
79fb3d0
Add page numbers
AMTuttle02 Jan 30, 2026
e6c16d9
Remove conversion tool
AMTuttle02 Jan 30, 2026
aef2f9b
Preserve page number on page load
AMTuttle02 Jan 30, 2026
d2cd784
Update version
AMTuttle02 Jan 30, 2026
3db7f65
Initial plan
Copilot Jan 30, 2026
1d21af6
Initial plan
Copilot Jan 30, 2026
3bc15d9
Update react/src/products/Products.jsx
AMTuttle02 Jan 30, 2026
01a2700
Update react/src/products/Products.jsx
AMTuttle02 Jan 30, 2026
45b1dc5
Initial plan
Copilot Jan 30, 2026
0d3bac3
Update react/src/products/Products.jsx
AMTuttle02 Jan 30, 2026
78b288d
Initial plan
Copilot Jan 30, 2026
1cb1ba6
Add backward compatibility routes for legacy product URLs
Copilot Jan 30, 2026
68ad499
Initial plan
Copilot Jan 30, 2026
1ce5d5a
Remove unused state variables and helper functions from Products.jsx
Copilot Jan 30, 2026
233961d
Merge pull request #309 from AMTuttle02/feat215_addPageNumbers
AMTuttle02 Jan 30, 2026
e73e39f
Consolidate duplicate CSS selectors for customDropdown and customDrop…
Copilot Jan 30, 2026
0012503
Merge pull request #310 from AMTuttle02/copilot/sub-pr-309
AMTuttle02 Jan 30, 2026
9ac7485
Merge pull request #311 from AMTuttle02/copilot/sub-pr-309-again
AMTuttle02 Jan 30, 2026
9bc31ce
Add missing semicolons to function declarations for consistency
Copilot Jan 30, 2026
acb9432
Merge pull request #312 from AMTuttle02/copilot/sub-pr-309-another-one
AMTuttle02 Jan 30, 2026
c988156
Add semicolon to buildParams function for consistency
Copilot Jan 30, 2026
eb1c2f1
Add semicolon to editProduct function for consistency
Copilot Jan 30, 2026
365691b
Merge pull request #314 from AMTuttle02/copilot/sub-pr-309-one-more-time
AMTuttle02 Jan 30, 2026
4e01d37
Merge branch 'feat215_addPageNumbers' into copilot/sub-pr-309-yet-again
AMTuttle02 Jan 30, 2026
ee1b33d
Merge pull request #313 from AMTuttle02/copilot/sub-pr-309-yet-again
AMTuttle02 Jan 30, 2026
efde813
Merge pull request #315 from AMTuttle02/feat215_addPageNumbers
AMTuttle02 Jan 30, 2026
1220fd9
Merge pull request #316 from AMTuttle02/dev
AMTuttle02 Feb 7, 2026
62cda1b
Fix mobile view for products page
AMTuttle02 Feb 9, 2026
7b683cc
Fix navbar dropdown closing
AMTuttle02 Feb 9, 2026
0596a2b
Merge pull request #317 from AMTuttle02/dev
AMTuttle02 Feb 10, 2026
18bae36
Fix navbar dropdown size on mobile
AMTuttle02 Feb 12, 2026
bc40d19
Merge pull request #318 from AMTuttle02/dev
AMTuttle02 Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion API/src/category/getProductCats.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}

if (isset($_SESSION['product_id'])) {
$query = $conn->prepare("SELECT categories FROM products WHERE product_id = ?;");
$query = $conn->prepare("SELECT categories, subcategories FROM products WHERE product_id = ?;");
$query->bind_param("s", $_SESSION["product_id"]);
if (!$query->execute()) {
// If insertion fails, return error message
Expand Down
11 changes: 6 additions & 5 deletions API/src/coupon/createCoupon.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$inputs = json_decode(file_get_contents('php://input'), true);

// Insert product to users cart
// Insert coupon; support separate categories and subcategories columns
$query = $conn->prepare(
"INSERT INTO coupons (code, description, amount, type, minimum_required, maximum_allowed, start_time, end_time, categories)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
"INSERT INTO coupons (code, description, amount, type, minimum_required, maximum_allowed, start_time, end_time, categories, subcategories)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$query->bind_param(
"sssssssss",
"ssssssssss",
$inputs['code'],
$inputs['description'],
$inputs['amount'],
Expand All @@ -25,7 +25,8 @@
$inputs['maximum_allowed'],
$inputs['start_time'],
$inputs['end_time'],
$inputs['categories']
$inputs['categories'],
$inputs['subcategories']
);

if (!$query->execute()) {
Expand Down
5 changes: 3 additions & 2 deletions API/src/coupon/updateCoupon.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

$query = $conn->prepare(
"UPDATE coupons
SET description = ?, amount = ?, type = ?, minimum_required = ?, maximum_allowed = ?, start_time = ?, end_time = ?, categories = ?
SET description = ?, amount = ?, type = ?, minimum_required = ?, maximum_allowed = ?, start_time = ?, end_time = ?, categories = ?, subcategories = ?
WHERE code = ?"
);
$query->bind_param(
"sssssssss",
"ssssssssss",
$inputs['description'],
$inputs['amount'],
$inputs['type'],
Expand All @@ -26,6 +26,7 @@
$inputs['start_time'],
$inputs['end_time'],
$inputs['categories'],
$inputs['subcategories'],
$inputs['code']
);

Expand Down
19 changes: 19 additions & 0 deletions API/src/product/getProductPopularity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header("Access-Control-Allow-Headers: X-Requested-With");

include '../admin/conn.php';

// Return product popularity as product_id => total_quantity
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$sql = "SELECT product_id, SUM(COALESCE(product_quantity,1)) AS qty FROM product_orders GROUP BY product_id";
$result = mysqli_query($conn, $sql);
$pop = [];
while ($row = mysqli_fetch_assoc($result)) {
$pop[$row['product_id']] = (int)$row['qty'];
}
echo json_encode($pop);
}

mysqli_close($conn);
8 changes: 5 additions & 3 deletions API/src/product/updateProductDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
$lColors = $_POST["lColors"];
$cColors = $_POST["cColors"];
$hColors = $_POST["hColors"];
$categories = $_POST["subcategories"];
// Accept new semicolon-separated id lists for categories and subcategories.
$categories = isset($_POST['categories']) ? $_POST['categories'] : (isset($_POST['subcategories']) ? $_POST['subcategories'] : '');
$subcategories = isset($_POST['subcategories']) && isset($_POST['categories']) ? $_POST['subcategories'] : '';
$defaultStyle = $_POST["default_style"];
$styleLocation = $_POST["default_style_location"];
$customDetailsRequired = $_POST["customFieldRequired"];
Expand All @@ -26,9 +28,9 @@

// Attempt to insert new design into table
$query = $conn->prepare("UPDATE products
SET product_name = ?, price = ?, tag_list = ?, tColors = ?, lColors = ?, cColors = ?, hColors = ?, categories = ?, default_style = ?, default_style_location = ?, CustomDetailsRequired = ?, sizesAvailable = ?
SET product_name = ?, price = ?, tag_list = ?, tColors = ?, lColors = ?, cColors = ?, hColors = ?, categories = ?, subcategories = ?, default_style = ?, default_style_location = ?, CustomDetailsRequired = ?, sizesAvailable = ?
WHERE product_id = ?;");
$query->bind_param("sssssssssssss", $productName, $price, $tags, $tColors, $lColors, $cColors, $hColors, $categories, $defaultStyle, $styleLocation, $customDetailsRequired, $sizeAvailable, $product_id);
$query->bind_param("ssssssssssssss", $productName, $price, $tags, $tColors, $lColors, $cColors, $hColors, $categories, $subcategories, $defaultStyle, $styleLocation, $customDetailsRequired, $sizeAvailable, $product_id);
if (!$query->execute()) {
// If insertion fails, return error message
echo json_encode("ERR: Insertion failed to execute" . $query->error);
Expand Down
11 changes: 7 additions & 4 deletions API/src/product/upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
$lColors = $_POST["lColors"];
$cColors = $_POST["cColors"];
$hColors = $_POST["hColors"];
$categories = $_POST["subcategories"];
// Accept either new 'categories' (ids) and 'subcategories' (ids),
// or legacy 'subcategories' (names). Prefer explicit 'categories' and 'subcategories'.
$categories = isset($_POST['categories']) ? $_POST['categories'] : (isset($_POST['subcategories']) ? $_POST['subcategories'] : '');
$subcategories = isset($_POST['subcategories']) && isset($_POST['categories']) ? $_POST['subcategories'] : '';
$defaultStyle = $_POST["default_style"];
$styleLocation = $_POST["style_location"];
$customFieldRequired = $_POST['customFieldRequired'];
Expand Down Expand Up @@ -49,9 +52,9 @@
}

// Attempt to insert new design into table
$query = $conn->prepare("INSERT INTO products (product_name, price, filename_front, filename_back, tag_list, tColors, lColors, cColors, hColors, categories, default_style, default_style_location, CustomDetailsRequired, sizesAvailable)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
$query->bind_param("ssssssssssssss", $productName, $price, $frontFileName, $backFileName, $tags, $tColors, $lColors, $cColors, $hColors, $categories, $defaultStyle, $styleLocation, $customFieldRequired, $sizeAvailable);
$query = $conn->prepare("INSERT INTO products (product_name, price, filename_front, filename_back, tag_list, tColors, lColors, cColors, hColors, categories, subcategories, default_style, default_style_location, CustomDetailsRequired, sizesAvailable)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
$query->bind_param("sssssssssssssss", $productName, $price, $frontFileName, $backFileName, $tags, $tColors, $lColors, $cColors, $hColors, $categories, $subcategories, $defaultStyle, $styleLocation, $customFieldRequired, $sizeAvailable);
if (!$query->execute()) {
// If insertion fails, return error message
die(json_encode("ERR: Insertion failed to execute" . $query->error));
Expand Down
4 changes: 2 additions & 2 deletions react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hometeamcreativity-website",
"homepage": "https://hometeamcreativity.com",
"version": "3.1.0",
"version": "4.0.0",
"description": "Website for Home Team Creativity",
"scripts": {
"dev": "vite",
Expand Down
108 changes: 90 additions & 18 deletions react/src/checkout/CheckoutDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,33 +99,101 @@ function CheckoutDetails() {
const determineDiscount = async (discount) => {
let orderTotal = order.total_cost;
let finalAmount = 0.00;

if (!discount.categories.includes("All")) {
// New logic: coupon may have separate `categories` (top-level names or ids) and `subcategories` (ids)
const couponCatRaw = (discount && discount.categories) ? discount.categories : '';
const couponSubRaw = (discount && discount.subcategories) ? discount.subcategories : '';
const isAll = (typeof couponCatRaw === 'string' && couponCatRaw.indexOf('All') !== -1) || couponCatRaw === 'All' || (typeof couponSubRaw === 'string' && couponSubRaw.indexOf('All') !== -1) || couponSubRaw === 'All';

if (!isAll) {
orderTotal = 0;
let oID = localStorage.getItem("oID") || 0;


// prepare coupon match lists
let couponSubIds = [];
let couponCatIds = [];
let couponCatNames = [];

// Normalize coupon subcategories and categories to support single id, semicolon lists, or legacy names
if (couponSubRaw != null && String(couponSubRaw).trim() !== '') {
couponSubIds = String(couponSubRaw).split(';').map(s => s.trim()).filter(Boolean);
}
if (couponCatRaw != null && String(couponCatRaw).trim() !== '') {
const parts = String(couponCatRaw).split(';').map(s => s.trim()).filter(Boolean);
const allNumeric = parts.length > 0 && parts.every(p => /^\d+$/.test(p));
if (allNumeric) {
couponCatIds = parts;
} else {
if (parts.length === 1 && parts[0].indexOf(' ') !== -1) {
couponCatNames = parts[0].split(' ').map(s => s.trim()).filter(Boolean);
} else {
couponCatNames = parts;
}
}
}

try {
const response = await fetch("/api/cart/getCart.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ order_id: oID }),
});
const data = await response.json();

for (const element of data) {
let categories = element.categories
.split(' ')
.filter(item => item.trim().length > 0)
.map(item => item.trim());

console.log(categories);
for (let j = 0; j < categories.length; ++j) {
if (discount.categories.includes(categories[j])) {
orderTotal += (GetProductPriceWithSize(element.price, element.product_type, element.size) * 1 * element.product_quantity);
console.log("Order Total Updated: " + orderTotal);
j = categories.length;
const elemCatRaw = element.categories || '';
const elemSubRaw = element.subcategories || '';

let elemSubIds = [];
let elemCatIds = [];
let elemCatNames = [];

// Normalize element subcategories: accept single id or semicolon-separated ids
if (elemSubRaw != null && String(elemSubRaw).trim() !== '') {
elemSubIds = String(elemSubRaw).split(';').map(s => s.trim()).filter(Boolean);
}

// Normalize element categories: accept semicolon-separated ids, single numeric id, or legacy names
if (elemCatRaw != null && String(elemCatRaw).trim() !== '') {
const parts = String(elemCatRaw).split(';').map(s => s.trim()).filter(Boolean);
const allNumeric = parts.length > 0 && parts.every(p => /^\d+$/.test(p));
if (allNumeric) {
elemCatIds = parts;
} else {
// If a single part contains spaces, split into names; otherwise treat parts as names
if (parts.length === 1 && parts[0].indexOf(' ') !== -1) {
elemCatNames = parts[0].split(' ').map(s => s.trim()).filter(Boolean);
} else {
elemCatNames = parts;
}
}
}

let matched = false;

// match by subcategory ids first
if (!matched && couponSubIds.length > 0 && elemSubIds.length > 0) {
for (let cs of couponSubIds) {
if (elemSubIds.includes(cs)) { matched = true; break; }
}
}

// match by top-level category ids
if (!matched && couponCatIds.length > 0 && elemCatIds.length > 0) {
for (let cc of couponCatIds) {
if (elemCatIds.includes(cc)) { matched = true; break; }
}
}

// match by top-level category names (legacy)
if (!matched && couponCatNames.length > 0 && elemCatNames.length > 0) {
for (let cn of couponCatNames) {
if (elemCatNames.includes(cn)) { matched = true; break; }
}
}

if (matched) {
orderTotal += (GetProductPriceWithSize(element.price, element.product_type, element.size) * 1 * element.product_quantity);
}
}
} catch (error) {
console.error("Error fetching cart data:", error);
Expand Down Expand Up @@ -163,9 +231,13 @@ function CheckoutDetails() {
.then((response) => response.json())
.then((data) => {
if (data) {
// verify code is active
if (currentDateTime.toLocaleString() < formatTime(data.start_time) || currentDateTime.toLocaleString() > formatTime(data.end_time)) {
throw(new Error(data.start_time + " - " + data.end_time));
// verify code is active by comparing Date objects (avoid locale-string comparison)
const startDate = new Date(data.start_time);
const endDate = data.end_time ? new Date(data.end_time) : null;
if (!isNaN(startDate.getTime())) {
if (currentDateTime < startDate || (endDate && !isNaN(endDate.getTime()) && currentDateTime > endDate)) {
throw(new Error(data.start_time + " - " + data.end_time));
}
}
determineDiscount(data);
}
Expand Down
56 changes: 55 additions & 1 deletion react/src/coupons/AllCoupons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import "./coupon.css";

function AllCoupons() {
const [coupons, setCoupons] = useState([]);
const [allSubcategories, setAllSubcategories] = useState([]);
const [subcatMap, setSubcatMap] = useState({});
const [allCategories, setAllCategories] = useState([]);
const [catMap, setCatMap] = useState({});
const navigate = useNavigate();

useEffect(() => {
Expand All @@ -15,6 +19,36 @@ function AllCoupons() {
});
}, []);

useEffect(() => {
fetch("/api/category/getSubCats.php")
.then((response) => response.json())
.then((data) => {
setAllSubcategories(data || []);
const map = {};
(data || []).forEach((s) => { map[String(s.id)] = s.name; });
setSubcatMap(map);
})
.catch(() => {
setAllSubcategories([]);
setSubcatMap({});
});
}, []);

useEffect(() => {
fetch("/api/category/getCategories.php")
.then((res) => res.json())
.then((data) => {
setAllCategories(data || []);
const map = {};
(data || []).forEach((c) => { map[String(c.id)] = c.category; });
setCatMap(map);
})
.catch(() => {
setAllCategories([]);
setCatMap({});
});
}, []);

const formatTime = (timeString) => {
const date = new Date(timeString);
if (isNaN(date.getTime())) {
Expand Down Expand Up @@ -57,7 +91,27 @@ function AllCoupons() {
<td className="hide-on-mobile">{coupon.maximum_allowed}</td>
<td>{formatTime(coupon.start_time)}</td>
<td>{formatTime(coupon.end_time)}</td>
<td className="hide-on-mobile">{coupon.categories}</td>
<td className="hide-on-mobile">
{(() => {
const cats = String(coupon.categories || '').trim();
if (cats === '') return '';
const parts = cats.split(';').map(s => s.trim()).filter(Boolean);
const allNumeric = parts.length > 0 && parts.every(p => /^\d+$/.test(p));
if (allNumeric) {
const names = parts.map(id => catMap[id] || id);
return names.join(', ');
}
return cats;
})()}
{coupon.subcategories ? ' | ' : ''}
{coupon.subcategories && (
(() => {
const ids = String(coupon.subcategories).split(';').map(s => s.trim()).filter(Boolean);
const names = ids.map(id => subcatMap[id] || id);
return names.join(', ');
})()
)}
</td>
<td>
<button className="default-button" onClick={() => navigate('/coupons/edit/' + coupon.code)}>
&nbsp;Edit&nbsp;
Expand Down
Loading