From d282dac2e95e2533d7b5af1a233b467de34ae189 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 26 May 2026 04:49:58 +0000 Subject: [PATCH] Sekut Bakery --- assets/css/custom.css | 1207 +++++++++++++++++++++++++++++------------ assets/js/main.js | 88 +-- cart.php | 110 ++++ cart_action.php | 47 ++ checkout.php | 142 +++++ index.php | 342 +++++++----- order_status.php | 217 ++++++++ product.php | 136 +++++ store.php | 830 ++++++++++++++++++++++++++++ 9 files changed, 2590 insertions(+), 529 deletions(-) create mode 100644 cart.php create mode 100644 cart_action.php create mode 100644 checkout.php create mode 100644 order_status.php create mode 100644 product.php create mode 100644 store.php diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..e0187d8 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,906 @@ +:root { + --bg: #f5f4f1; + --surface: #ffffff; + --surface-alt: #f8f7f4; + --line: #e7e5e4; + --line-strong: #d6d3d1; + --text: #171717; + --muted: #57534e; + --accent: #2b2927; + --accent-soft: #efeeea; + --success: #166534; + --warning: #92400e; + --danger: #991b1b; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 18px; + --shadow-sm: 0 1px 2px rgba(17, 24, 39, 0.04); + --shadow-md: 0 16px 40px -32px rgba(17, 24, 39, 0.5); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; margin: 0; min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +img { + max-width: 100%; + display: block; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +strong { + font-weight: 600; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; +small, +.small { + color: var(--muted); +} + +.text-muted { + color: var(--muted) !important; +} + +a { + color: var(--text); + text-decoration: none; +} + +.site-header { + background: rgba(245, 244, 241, 0.92); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(12px); +} + +.navbar { + padding: 0.875rem 0; +} + +.navbar-toggler { + border-radius: var(--radius-sm); + border-color: var(--line-strong); +} + +.navbar-toggler:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 23, 23, 0.08); +} + +.brand-mark { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.brand-mark__title { + font-size: 0.95rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.brand-mark__subtitle { + font-size: 0.77rem; + color: var(--muted); +} + +.nav-link { + color: var(--muted); + font-weight: 500; + border-radius: 999px; + padding: 0.5rem 0.875rem !important; +} + +.nav-link:hover, +.nav-link.active { + color: var(--text); + background: var(--surface); + box-shadow: inset 0 0 0 1px var(--line); +} + +.btn, +.form-control, +textarea { + border-radius: var(--radius-sm); +} + +.btn { + font-weight: 600; + padding: 0.75rem 1rem; +} + +.btn:focus-visible, +.form-control:focus-visible, +textarea:focus-visible, +.choice-card:focus-visible, +.filter-pill:focus-visible { + outline: 2px solid rgba(23, 23, 23, 0.12); + outline-offset: 2px; +} + +.btn-dark { + background: var(--accent); + border-color: var(--accent); +} + +.btn-dark:hover, +.btn-dark:focus { + background: #111111; + border-color: #111111; +} + +.btn-outline-secondary { + color: var(--text); + border-color: var(--line-strong); + background: var(--surface); +} + +.btn-outline-secondary:hover, +.btn-outline-secondary:focus { + color: var(--text); + background: var(--surface-alt); + border-color: var(--text); +} + +.btn-lg { + padding: 0.9rem 1.15rem; +} + +.btn-cart { + box-shadow: var(--shadow-sm); +} + +.form-control, +textarea { + border: 1px solid var(--line-strong); + padding: 0.75rem 0.9rem; + background: var(--surface); +} + +.form-control:focus, +textarea:focus { + border-color: var(--text); + box-shadow: 0 0 0 0.2rem rgba(23, 23, 23, 0.08); +} + +.invalid-feedback { + font-size: 0.85rem; +} + +.site-main { + padding-bottom: 2rem; +} + +.hero-panel, +.surface-panel, +.summary-card, +.product-card, +.metric-card, +.feature-card, +.step-card, +.empty-state-card, +.cart-item, +.info-box, +.choice-card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.hero-panel { + padding: clamp(1.25rem, 2vw, 2.5rem); +} + +.surface-panel, +.summary-card, +.feature-card, +.step-card, +.empty-state-card { + padding: 1.5rem; +} + +.section-block { + padding: 1rem 0 2rem; +} + +.eyebrow { + display: inline-flex; + align-items: center; + padding: 0.4rem 0.65rem; + border-radius: 999px; + background: var(--accent-soft); + color: var(--muted); + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.display-title { + font-size: clamp(2.25rem, 4vw, 4rem); + line-height: 1; + font-weight: 800; + letter-spacing: -0.04em; + margin: 1rem 0; + max-width: 12ch; +} + +.lead-copy { + color: var(--muted); + max-width: 62ch; + font-size: 1.05rem; + line-height: 1.7; +} + +.section-title { + font-size: clamp(1.65rem, 2.4vw, 2.5rem); + line-height: 1.1; + letter-spacing: -0.03em; + font-weight: 700; + margin: 0.65rem 0 0.75rem; +} + +.section-copy { + color: var(--muted); + max-width: 60ch; + line-height: 1.7; +} + +.metric-card { + padding: 1.1rem 1rem; + height: 100%; +} + +.metric-value { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.03em; +} + +.metric-value--small { + font-size: 1.2rem; +} + +.metric-label { + margin-top: 0.4rem; + font-size: 0.88rem; + color: var(--muted); +} + +.card-kicker { + font-size: 0.77rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + font-weight: 600; + margin-bottom: 0.6rem; +} + +.summary-title { + font-size: 1.45rem; + font-weight: 700; + letter-spacing: -0.03em; + margin-bottom: 1rem; +} + +.list-clean { + list-style: none; + padding: 0; + margin: 0; +} + +.compact-list { + display: grid; + gap: 0.85rem; +} + +.compact-list li { + display: flex; + gap: 0.875rem; + align-items: flex-start; + color: var(--muted); +} + +.list-index { + width: 1.75rem; + height: 1.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--accent-soft); + border: 1px solid var(--line); + border-radius: 999px; + color: var(--text); + font-size: 0.82rem; + font-weight: 600; + flex-shrink: 0; +} + +.receipt-card { + background: var(--surface-alt); + border: 1px solid var(--line); + border-radius: var(--radius-md); + padding: 1rem 1.1rem; +} + +.receipt-card--flat { + background: transparent; + padding: 0; + border: 0; +} + +.receipt-card__head { + display: flex; + justify-content: space-between; + gap: 1rem; + font-size: 0.92rem; + color: var(--muted); + margin-bottom: 0.65rem; +} + +.receipt-line { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + padding: 0.75rem 0; + border-top: 1px solid var(--line); + color: var(--muted); +} + +.receipt-line:first-of-type { + border-top: 0; + padding-top: 0; +} + +.receipt-line strong { + color: var(--text); + font-weight: 600; + text-align: right; +} + +.receipt-line--total { + margin-top: 0.25rem; + padding-top: 1rem; + border-top: 1px solid var(--line-strong); + font-size: 1rem; +} + +.feature-card__title, +.step-card h3 { + font-size: 1.02rem; + font-weight: 700; + margin-bottom: 0.55rem; +} + +.feature-card__copy, +.step-card p, +.note-copy, +.info-box p { + color: var(--muted); + line-height: 1.7; + margin: 0; +} + +.step-card__number { + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 0.85rem; +} + +.filter-pills { + overflow-x: auto; + padding-bottom: 0.125rem; +} + +.filter-pill { + display: inline-flex; + align-items: center; + padding: 0.65rem 0.95rem; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--surface); + color: var(--muted); + font-size: 0.9rem; + white-space: nowrap; + transition: all 0.2s ease; +} + +.filter-pill:hover, +.filter-pill.is-active { + color: var(--text); + border-color: var(--text); +} + +.product-card { display: flex; flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); overflow: hidden; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; +.product-card__body { display: flex; + flex-direction: column; + flex: 1; + padding: 1.2rem; +} + +.product-card__title { + font-size: 1.1rem; + font-weight: 700; + margin: 0; +} + +.product-card__copy { + color: var(--muted); + line-height: 1.7; + margin-bottom: 1rem; +} + +.product-visual { + border-bottom: 1px solid var(--line); + padding: 1.2rem; + min-height: 15rem; + display: flex; + flex-direction: column; justify-content: space-between; + background: var(--surface-alt); +} + +.product-visual--large { + min-height: 24rem; + border: 1px solid var(--line); + border-radius: calc(var(--radius-lg) - 4px); +} + +.product-visual--mini { + min-height: 8.5rem; + width: 12rem; + border: 1px solid var(--line); + border-radius: var(--radius-md); + flex-shrink: 0; +} + +.product-visual--ink { + background: #efefee; +} + +.product-visual--stone { + background: #f4f2ee; +} + +.product-visual--sand { + background: #f3efe6; +} + +.product-visual--taupe { + background: #ece7df; +} + +.product-visual__meta { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.product-visual__code { + font-size: clamp(2.5rem, 6vw, 5rem); + letter-spacing: -0.08em; + line-height: 1; + font-weight: 800; + color: rgba(23, 23, 23, 0.85); +} + +.product-visual__name { + font-size: 0.95rem; + color: var(--muted); +} + +.price-tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.45rem 0.75rem; + border: 1px solid var(--line); + border-radius: 999px; + font-weight: 600; + white-space: nowrap; + background: var(--surface-alt); +} + +.price-tag--inline { + display: inline-flex; +} + +.detail-chip-group { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.detail-chip { + display: inline-flex; + align-items: center; + padding: 0.4rem 0.65rem; + background: var(--accent-soft); + border: 1px solid var(--line); + border-radius: 999px; + font-size: 0.82rem; + color: var(--muted); +} + +.sticky-summary { + position: sticky; + top: 6rem; +} + +.breadcrumb { + --bs-breadcrumb-divider: '/'; +} + +.breadcrumb a { + color: var(--muted); +} + +.breadcrumb-item.active { + color: var(--text); +} + +.price-block__amount { + font-size: clamp(1.9rem, 4vw, 3rem); + font-weight: 800; + letter-spacing: -0.05em; +} + +.price-block__caption { + color: var(--muted); + margin-top: 0.35rem; +} + +.detail-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.detail-grid > div { + border: 1px solid var(--line); + border-radius: var(--radius-md); + padding: 0.9rem 1rem; + background: var(--surface-alt); +} + +.detail-grid__label { + display: block; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + margin-bottom: 0.3rem; +} + +.purchase-box { + padding: 1rem; + background: var(--surface-alt); + border: 1px solid var(--line); + border-radius: var(--radius-md); +} + +.quantity-field { + display: grid; + grid-template-columns: 48px minmax(84px, 110px) 48px; + gap: 0.75rem; align-items: center; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; +.quantity-field .btn { + padding: 0.55rem 0.75rem; +} + +.quantity-field .form-control { + font-weight: 600; +} + +.quantity-field--compact { + grid-template-columns: 40px 84px 40px; + gap: 0.5rem; +} + +.section-divider { + height: 1px; + background: var(--line); + margin: 1.5rem 0; +} + +.choice-card { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 0.35rem; + padding: 1rem; + height: 100%; + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.2s ease; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.choice-title { + font-weight: 600; + color: var(--text); } -::-webkit-scrollbar-track { - background: transparent; +.choice-copy { + font-size: 0.9rem; + color: var(--muted); + line-height: 1.6; } -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; +.btn-check:checked + .choice-card { + border-color: var(--text); + box-shadow: inset 0 0 0 1px var(--text); + background: var(--surface-alt); } -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); +.empty-state-card { + padding: min(8vw, 3rem); + max-width: 52rem; } -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { +.cart-item { display: flex; + gap: 1rem; + padding: 1rem; +} + +.cart-item__body { + flex: 1; +} + +.cart-item__title { + font-size: 1.05rem; + margin: 0; + font-weight: 700; +} + +.cart-item__total { + font-weight: 700; + font-size: 1rem; +} + +.info-box { + padding: 1rem 1.1rem; + background: var(--surface-alt); + border-radius: var(--radius-md); +} + +.order-table thead th { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + border-bottom: 1px solid var(--line); +} + +.order-table tbody td, +.order-table tfoot td { + border-bottom: 1px solid var(--line); + padding: 0.85rem 0; +} + +.order-table tfoot tr:last-child td { + border-bottom: 0; + padding-bottom: 0; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.45rem 0.75rem; + border-radius: 999px; + font-size: 0.85rem; + font-weight: 600; + border: 1px solid var(--line); + background: var(--surface-alt); +} + +.status-pill--pending { + background: #fff8eb; + border-color: #f3d28a; + color: #8a5a00; +} + +.status-pill--processing { + background: #f4f4f5; + border-color: #d4d4d8; + color: #3f3f46; +} + +.status-pill--shipping { + background: #eef2ff; + border-color: #c7d2fe; + color: #3730a3; +} + +.status-pill--done { + background: #ecfdf3; + border-color: #bbf7d0; + color: #166534; +} + +.status-pill--cancelled { + background: #fef2f2; + border-color: #fecaca; + color: #991b1b; +} + +.status-legend { + display: grid; gap: 0.75rem; } -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; +.status-legend__item { + display: grid; + gap: 0.4rem; } -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; +.timeline-list { + display: grid; gap: 1rem; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); +.timeline-step { + display: flex; + gap: 1rem; padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--surface-alt); + opacity: 0.72; } -.history-table { - width: 100%; +.timeline-step__dot { + width: 0.9rem; + height: 0.9rem; + margin-top: 0.35rem; + border-radius: 999px; + background: var(--line-strong); + flex-shrink: 0; } -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; +.timeline-step__title { + font-weight: 600; + margin-bottom: 0.25rem; } -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; +.timeline-step__copy { + color: var(--muted); + line-height: 1.6; } -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; +.timeline-step.is-complete, +.timeline-step.is-current { + opacity: 1; + border-color: var(--line-strong); } -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file +.timeline-step.is-complete .timeline-step__dot { + background: var(--success); +} + +.timeline-step.is-current .timeline-step__dot { + background: var(--accent); + box-shadow: 0 0 0 4px rgba(23, 23, 23, 0.08); +} + +.toast-stack { + z-index: 1080; +} + +.toast-theme { + background: rgba(23, 23, 23, 0.96); + color: #ffffff; + border: 0; + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); +} + +.toast-theme--success { + background: rgba(22, 101, 52, 0.96); +} + +.toast-theme--warning { + background: rgba(146, 64, 14, 0.96); +} + +.toast-theme--danger { + background: rgba(153, 27, 27, 0.96); +} + +.site-footer { + border-top: 1px solid var(--line); + background: rgba(255, 255, 255, 0.7); +} + +.footer-title { + font-size: 0.95rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 0.35rem; +} + +.footer-copy { + color: var(--muted); + margin: 0; + max-width: 36rem; + line-height: 1.6; +} + +.footer-links a { + color: var(--muted); + font-weight: 500; +} + +.footer-links a:hover { + color: var(--text); +} + +.alert { + border-radius: var(--radius-md); +} + +@media (max-width: 991.98px) { + .sticky-summary { + position: static; + } + + .product-visual--mini { + width: 100%; + min-height: 10rem; + } +} + +@media (max-width: 767.98px) { + .hero-panel, + .surface-panel, + .summary-card, + .feature-card, + .step-card, + .empty-state-card, + .cart-item, + .product-card__body { + padding: 1rem; + } + + .display-title { + max-width: none; + } + + .detail-grid { + grid-template-columns: 1fr; + } + + .cart-item { + flex-direction: column; + } + + .quantity-field { + grid-template-columns: 44px minmax(72px, 1fr) 44px; + } +} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..a11200a 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,63 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + if (window.bootstrap && window.bootstrap.Toast) { + document.querySelectorAll('.toast').forEach((element) => { + const toast = new window.bootstrap.Toast(element); + toast.show(); + }); + } - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; + document.querySelectorAll('[data-qty-target][data-qty-step]').forEach((button) => { + button.addEventListener('click', () => { + const targetId = button.getAttribute('data-qty-target'); + const input = targetId ? document.getElementById(targetId) : null; + if (!input) { + return; + } - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + const min = parseInt(input.getAttribute('min') || '1', 10); + const max = parseInt(input.getAttribute('max') || '20', 10); + const step = parseInt(button.getAttribute('data-qty-step') || '0', 10); + const current = parseInt(input.value || String(min), 10) || min; + const next = Math.max(min, Math.min(max, current + step)); + input.value = String(next); + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + }); - appendMessage(message, 'visitor'); - chatInput.value = ''; + document.querySelectorAll('[data-copy-text]').forEach((button) => { + button.addEventListener('click', async () => { + const value = button.getAttribute('data-copy-text'); + if (!value || !navigator.clipboard) { + return; + } - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + const originalText = button.textContent; + try { + await navigator.clipboard.writeText(value); + button.textContent = 'Tersalin'; + button.classList.remove('btn-outline-success'); + button.classList.add('btn-success'); + window.setTimeout(() => { + button.textContent = originalText; + button.classList.remove('btn-success'); + button.classList.add('btn-outline-success'); + }, 1500); + } catch (error) { + console.error('Clipboard error', error); + } + }); + }); + + document.querySelectorAll('[data-auto-disable]').forEach((form) => { + form.addEventListener('submit', () => { + const submitButton = form.querySelector('button[type="submit"]'); + if (!submitButton || submitButton.disabled) { + return; + } + + submitButton.dataset.originalText = submitButton.innerHTML; + submitButton.disabled = true; + submitButton.innerHTML = 'Memproses…'; + }); }); }); diff --git a/cart.php b/cart.php new file mode 100644 index 0000000..fe77039 --- /dev/null +++ b/cart.php @@ -0,0 +1,110 @@ + true]); +?> +
+
+ Keranjang +

Tinjau pesanan sebelum checkout.

+

Update quantity, cek ongkir, lalu lanjut ke checkout untuk menyimpan pesanan ke sistem.

+
+ + +
+ Keranjang kosong +

Belum ada produk di keranjang.

+

Mulai dari katalog untuk mencoba alur add-to-cart lalu kembali ke halaman ini.

+ +
+ +
+
+
+ + + +
+
+ + + +
+
+
+
+

+

+
+ + +
+
+
+
+
per
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+ +
+ Tambah produk lain + +
+
+ + +
+ + + +
+ +
+
+ +
+
+ +
+ diff --git a/cart_action.php b/cart_action.php new file mode 100644 index 0000000..df0283f --- /dev/null +++ b/cart_action.php @@ -0,0 +1,47 @@ + true]); +?> +
+
+ Checkout +

Simpan pesanan ke sistem.

+

Status awal pesanan adalah Menunggu Pembayaran. Instruksi pembayaran akan tampil setelah order berhasil dibuat.

+
+ + + + + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+ +
+
+
+

Metode pembayaran

+

Pilih salah satu. Instruksi lengkap muncul di halaman status pesanan.

+
+
+
+ $method): ?> +
+
+ > + +
+
+ +
+ +
+ +
+ +
+ Kembali ke keranjang + +
+
+
+
+ +
+
+
+ diff --git a/index.php b/index.php index 7205f3d..973c86d 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,202 @@ - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+
+
+ Initial MVP slice +

Belanja bakery tanpa alur yang berantakan.

+

+ Katalog, keranjang, checkout, dan halaman status pesanan sekarang terhubung dalam satu alur yang rapi. + Cocok untuk validasi toko online berbasis PHP + MySQL sebelum lanjut ke admin panel penuh. +

+ +
+
+
+
+
produk demo siap dijual
+
+
+
+
+
1 tabel
+
pesanan tersimpan di MySQL
+
+
+
+
+
4 status
+
progress order yang mudah dipahami
+
+
+
+
+
+ +
-
- - - + + +
+
+
+
+
Katalog yang langsung bisa dijual
+

Ada kategori, harga, deskripsi singkat, dan halaman detail produk untuk tiap item.

+
+
+
+
+
Checkout tersimpan di sistem
+

Order number dibuat otomatis, item pesanan disimpan di MySQL, dan status awal langsung tercatat.

+
+
+
+
+
Status pesanan mandiri
+

Pelanggan cukup masukkan kode pesanan dan email untuk melihat progress dan instruksi pembayaran.

+
+
+
+
+ +
+
+
+ Katalog Produk +

Pilihan cake, bread, dan pastry untuk toko demo.

+

Gunakan katalog ini sebagai starting point sebelum menambahkan admin, stok, kupon, dan pembayaran real.

+
+
+ $category): ?> + + + + + +
+
+ +
+ +
+
+
+ + + +
+
+
+
+

+

+
+ +
+

+
+ + + +
+
+ Lihat detail +
+ + + + + +
+
+
+
+
+ +
+
+ +
+
+ Cara Order +

Alur sederhana yang siap dipakai sebagai base toko online.

+
+
+
+
+
01
+

Pilih item

+

Pelanggan lihat katalog, buka detail produk, lalu tambahkan item ke keranjang.

+
+
+
+
+
02
+

Checkout

+

Isi nama, email, telepon, alamat, dan metode pembayaran. Order number dibuat otomatis.

+
+
+
+
+
03
+

Lacak status

+

Gunakan order number dan email untuk melihat status pesanan dan instruksi pembayaran kapan saja.

+
+
+
+
+ diff --git a/order_status.php b/order_status.php new file mode 100644 index 0000000..7a4add6 --- /dev/null +++ b/order_status.php @@ -0,0 +1,217 @@ + true]); +?> +
+
+ Status Pesanan +

Lacak progress pesanan pelanggan.

+

Masukkan kode pesanan dan email untuk melihat item, total, metode pembayaran, dan status terbaru.

+
+ + + + + +
+
+
+
Cek pesanan
+

Cari berdasarkan kode & email

+
+ + +
+
+ + +
+
+ + Belanja lagi +
+ +
+
Status default
+
+ +
+ +

+
+ +
+
+
+
+
+ + + + + +
+
+
+
Order detail
+

+

Dibuat pada

+
+ +
+ +
+
+
+
+
total pembayaran
+
+
+
+
+
+
metode bayar
+
+
+
+
+
item
+
jumlah item di order
+
+
+
+ +
+
+

Item pesanan

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProdukQtyTotal
+ +
/ item
+
Subtotal
Ongkir
Grand total
+
+
+
+
+
Data penerima
+

+

+

+

+
+
+
Instruksi pembayaran
+

+ +
Catatan pelanggan:
+ +
+
+
+
+ + + +
+
Progress pesanan
+
+ $step): ?> + +
+
+
+
+

+
+
+ +
+
+ + +
+ Belum ada detail +

Masukkan kode pesanan untuk melihat status.

+

Setelah checkout berhasil, halaman ini akan menampilkan ringkasan item, total pembayaran, dan progress status order.

+
    +
  • 1Masuk dari checkout otomatis akan mengisi kode pesanan terbaru.
  • +
  • 2Untuk kunjungan berikutnya, pelanggan cukup ingat order number dan email.
  • +
  • 3Status berikutnya bisa diubah admin di iterasi lanjutan.
  • +
+
+ +
+
+
+ diff --git a/product.php b/product.php new file mode 100644 index 0000000..a281e70 --- /dev/null +++ b/product.php @@ -0,0 +1,136 @@ + true]); + ?> +
+ 404 +

Produk tidak ditemukan.

+

Slug produk tidak valid atau item sudah dihapus dari katalog demo.

+ +
+ + + +
+
+
+
+
+ + + +
+
+ +
+
+
+
informasi cepat
+
+
+ +
+
+
+
+
+ +

+

+
+
+
per
+
+ +
+
+ Lead time + +
+
+ Porsi + +
+
+ Kategori + +
+
+ +
+ + + + +
+ + + +
+
+ + Lihat keranjang +
+
+ +
+
Catatan checkout
+

Saat checkout, pelanggan akan memilih metode pembayaran dan sistem langsung membuat order number untuk pelacakan status.

+
+
+
+
+
+ +
+
+ Produk terkait +

Tambahkan item lain untuk melengkapi pesanan.

+
+
+ +
+ +
+ +
+
+ diff --git a/store.php b/store.php new file mode 100644 index 0000000..d13b7c7 --- /dev/null +++ b/store.php @@ -0,0 +1,830 @@ + [ + 'label' => 'Semua', + 'description' => 'Seluruh katalog bakery untuk daily order dan pre-order.', + ], + 'signature-cakes' => [ + 'label' => 'Signature Cakes', + 'description' => 'Whole cake dan sliced cake untuk momen spesial.', + ], + 'artisan-bread' => [ + 'label' => 'Artisan Bread', + 'description' => 'Roti harian dengan tekstur bersih dan fermentasi lebih lama.', + ], + 'cookies-pastry' => [ + 'label' => 'Cookies & Pastry', + 'description' => 'Pilihan pastry dan cookies untuk coffee break dan hampers.', + ], + ]; +} + +function store_products(): array +{ + return [ + 'burnt-cheesecake' => [ + 'slug' => 'burnt-cheesecake', + 'name' => 'Burnt Cheesecake', + 'category' => 'signature-cakes', + 'category_label' => 'Signature Cakes', + 'price' => 185000.0, + 'unit' => 'whole cake', + 'short_description' => 'Cheesecake creamy dengan permukaan karamel gelap dan finish yang clean.', + 'description' => 'Whole cake 18 cm dengan rasa vanilla-cream cheese yang lembut dan lapisan atas yang sedikit smoky. Cocok untuk 8–10 porsi dan ideal untuk dessert keluarga.', + 'lead_time' => 'Pre-order 1 hari', + 'serves' => '8–10 porsi', + 'tone' => 'ink', + 'visual_code' => 'BC', + 'highlights' => ['18 cm', 'Best seller', 'Pre-order 1 hari'], + ], + 'matcha-roll-cake' => [ + 'slug' => 'matcha-roll-cake', + 'name' => 'Matcha Roll Cake', + 'category' => 'signature-cakes', + 'category_label' => 'Signature Cakes', + 'price' => 158000.0, + 'unit' => 'roll cake', + 'short_description' => 'Sponge cake tipis dengan filling krim matcha yang ringan.', + 'description' => 'Roll cake dengan sponge lembut, rasa matcha yang clean, dan krim yang tidak terlalu manis. Cocok untuk sharing di meeting kecil atau hadiah praktis.', + 'lead_time' => 'Ready setiap pagi', + 'serves' => '6–8 potong', + 'tone' => 'stone', + 'visual_code' => 'MR', + 'highlights' => ['6–8 potong', 'Fresh daily', 'Light sweetness'], + ], + 'sesame-sourdough' => [ + 'slug' => 'sesame-sourdough', + 'name' => 'Sesame Sourdough Loaf', + 'category' => 'artisan-bread', + 'category_label' => 'Artisan Bread', + 'price' => 52000.0, + 'unit' => 'loaf', + 'short_description' => 'Roti sourdough dengan crust renyah, crumb lembut, dan taburan wijen.', + 'description' => 'Loaf fermentasi alami dengan aroma gandum yang lebih dalam. Enak untuk sandwich, toast sarapan, atau stok roti rumah selama 2–3 hari.', + 'lead_time' => 'Ready setiap siang', + 'serves' => '8 slice tebal', + 'tone' => 'sand', + 'visual_code' => 'SD', + 'highlights' => ['Fermentasi alami', '8 slice', 'Crust renyah'], + ], + 'focaccia-olive' => [ + 'slug' => 'focaccia-olive', + 'name' => 'Focaccia Tomato Olive', + 'category' => 'artisan-bread', + 'category_label' => 'Artisan Bread', + 'price' => 48000.0, + 'unit' => 'tray', + 'short_description' => 'Focaccia savory dengan olive oil, tomat, dan olive hitam.', + 'description' => 'Roti focaccia bertekstur empuk dengan permukaan yang sedikit garing. Cocok untuk snack meeting, teman sup, atau dibuat sandwich terbuka.', + 'lead_time' => 'Ready terbatas', + 'serves' => '4–5 porsi', + 'tone' => 'taupe', + 'visual_code' => 'FO', + 'highlights' => ['Savory', '4–5 porsi', 'Ready terbatas'], + ], + 'butter-croissant' => [ + 'slug' => 'butter-croissant', + 'name' => 'Butter Croissant', + 'category' => 'cookies-pastry', + 'category_label' => 'Cookies & Pastry', + 'price' => 28000.0, + 'unit' => 'pcs', + 'short_description' => 'Croissant berlapis dengan aroma butter dan tekstur flaky.', + 'description' => 'Pastry klasik dengan lapisan tipis yang renyah di luar dan ringan di dalam. Cocok untuk sarapan cepat atau pairing dengan kopi hitam.', + 'lead_time' => 'Ready dari pagi', + 'serves' => '1 pcs besar', + 'tone' => 'stone', + 'visual_code' => 'CR', + 'highlights' => ['Flaky', 'Fresh baked', 'Coffee pairing'], + ], + 'almond-cookies-tin' => [ + 'slug' => 'almond-cookies-tin', + 'name' => 'Almond Cookies Tin', + 'category' => 'cookies-pastry', + 'category_label' => 'Cookies & Pastry', + 'price' => 68000.0, + 'unit' => 'tin', + 'short_description' => 'Cookies renyah dengan potongan almond panggang dalam kemasan kaleng.', + 'description' => 'Cookies butter dengan tekstur crisp dan rasa toasted almond yang hangat. Praktis untuk hampers kecil, pantry kantor, atau stok camilan rumah.', + 'lead_time' => 'Ready stock', + 'serves' => '20–24 pcs', + 'tone' => 'ink', + 'visual_code' => 'AC', + 'highlights' => ['20–24 pcs', 'Giftable', 'Ready stock'], + ], + ]; +} + +function store_product(string $slug): ?array +{ + $products = store_products(); + return $products[$slug] ?? null; +} + +function store_filtered_products(string $category = 'all'): array +{ + $products = array_values(store_products()); + if ($category === 'all' || !isset(store_categories()[$category])) { + return $products; + } + + return array_values(array_filter( + $products, + static fn(array $product): bool => $product['category'] === $category + )); +} + +function store_related_products(string $slug, string $category, int $limit = 3): array +{ + $products = array_values(array_filter( + store_products(), + static fn(array $product): bool => $product['slug'] !== $slug && $product['category'] === $category + )); + + if (count($products) < $limit) { + $fallback = array_values(array_filter( + store_products(), + static fn(array $product): bool => $product['slug'] !== $slug && $product['category'] !== $category + )); + $products = array_merge($products, $fallback); + } + + return array_slice($products, 0, $limit); +} + +function store_cart(): array +{ + if (!isset($_SESSION[STORE_CART_KEY]) || !is_array($_SESSION[STORE_CART_KEY])) { + $_SESSION[STORE_CART_KEY] = []; + } + + return $_SESSION[STORE_CART_KEY]; +} + +function store_set_cart(array $cart): void +{ + $_SESSION[STORE_CART_KEY] = $cart; +} + +function store_cart_count(): int +{ + return array_sum(store_cart()); +} + +function store_add_to_cart(string $slug, int $quantity = 1): bool +{ + $product = store_product($slug); + if (!$product) { + return false; + } + + $quantity = max(1, min(20, $quantity)); + $cart = store_cart(); + $cart[$slug] = min(20, ((int)($cart[$slug] ?? 0)) + $quantity); + store_set_cart($cart); + + return true; +} + +function store_update_cart(array $quantities): void +{ + $products = store_products(); + $updated = []; + + foreach ($quantities as $slug => $quantity) { + if (!isset($products[$slug])) { + continue; + } + + $qty = (int)$quantity; + if ($qty > 0) { + $updated[$slug] = min(20, $qty); + } + } + + store_set_cart($updated); +} + +function store_remove_from_cart(string $slug): void +{ + $cart = store_cart(); + unset($cart[$slug]); + store_set_cart($cart); +} + +function store_money(float $amount): string +{ + return 'Rp ' . number_format($amount, 0, ',', '.'); +} + +function store_substr(string $value, int $start, int $length): string +{ + if (function_exists('mb_substr')) { + return (string)mb_substr($value, $start, $length); + } + + return (string)substr($value, $start, $length); +} + +function store_strlen(string $value): int +{ + if (function_exists('mb_strlen')) { + return (int)mb_strlen($value); + } + + return strlen($value); +} + +function store_lower(string $value): string +{ + if (function_exists('mb_strtolower')) { + return (string)mb_strtolower($value); + } + + return strtolower($value); +} + +function store_cart_summary(): array +{ + $products = store_products(); + $lines = []; + + foreach (store_cart() as $slug => $quantity) { + if (!isset($products[$slug])) { + continue; + } + + $product = $products[$slug]; + $lineTotal = ((float)$product['price']) * (int)$quantity; + $lines[] = [ + 'slug' => $slug, + 'quantity' => (int)$quantity, + 'product' => $product, + 'line_total' => $lineTotal, + ]; + } + + $subtotal = array_reduce( + $lines, + static fn(float $carry, array $line): float => $carry + (float)$line['line_total'], + 0.0 + ); + + $shippingFee = $subtotal <= 0 ? 0.0 : ($subtotal >= 250000 ? 0.0 : 18000.0); + + return [ + 'lines' => $lines, + 'subtotal' => $subtotal, + 'shipping_fee' => $shippingFee, + 'grand_total' => $subtotal + $shippingFee, + ]; +} + +function store_payment_methods(): array +{ + return [ + 'bank_transfer' => [ + 'label' => 'Transfer Bank', + 'description' => 'Instruksi pembayaran tampil setelah checkout selesai.', + 'instruction' => 'Transfer ke rekening demo bakery lalu simpan bukti bayar untuk verifikasi admin.', + ], + 'qris' => [ + 'label' => 'QRIS', + 'description' => 'Praktis untuk mobile banking dan e-wallet.', + 'instruction' => 'Gunakan QRIS demo yang diberikan admin setelah pesanan masuk ke sistem.', + ], + 'cod' => [ + 'label' => 'Bayar di Tempat', + 'description' => 'Tersedia untuk pengantaran area terdekat.', + 'instruction' => 'Siapkan nominal pas saat kurir mengantarkan pesanan ke alamat Anda.', + ], + ]; +} + +function store_status_steps(): array +{ + return [ + [ + 'value' => 'Menunggu Pembayaran', + 'label' => 'Menunggu Pembayaran', + 'description' => 'Pesanan tersimpan dan menunggu pembayaran pelanggan.', + ], + [ + 'value' => 'Diproses', + 'label' => 'Diproses', + 'description' => 'Kitchen sedang menyiapkan item sesuai pesanan.', + ], + [ + 'value' => 'Dikirim', + 'label' => 'Dikirim', + 'description' => 'Pesanan sedang diantar ke alamat tujuan.', + ], + [ + 'value' => 'Selesai', + 'label' => 'Selesai', + 'description' => 'Pesanan telah diterima pelanggan.', + ], + ]; +} + +function store_status_index(string $status): int +{ + foreach (store_status_steps() as $index => $step) { + if ($step['value'] === $status) { + return $index; + } + } + + return -1; +} + +function store_status_class(string $status): string +{ + return match ($status) { + 'Menunggu Pembayaran' => 'status-pill status-pill--pending', + 'Diproses' => 'status-pill status-pill--processing', + 'Dikirim' => 'status-pill status-pill--shipping', + 'Selesai' => 'status-pill status-pill--done', + 'Batal' => 'status-pill status-pill--cancelled', + default => 'status-pill', + }; +} + +function store_flash(string $type, string $message): void +{ + $_SESSION[STORE_FLASH_KEY][] = [ + 'type' => $type, + 'message' => $message, + ]; +} + +function store_consume_flashes(): array +{ + $flashes = $_SESSION[STORE_FLASH_KEY] ?? []; + unset($_SESSION[STORE_FLASH_KEY]); + + return is_array($flashes) ? $flashes : []; +} + +function store_input_class(array $errors, string $field): string +{ + return isset($errors[$field]) ? ' is-invalid' : ''; +} + +function store_checkout_defaults(): array +{ + return [ + 'customer_name' => '', + 'email' => '', + 'phone' => '', + 'address' => '', + 'note' => '', + 'payment_method' => 'bank_transfer', + ]; +} + +function store_sanitize_line(string $value, int $max = 255): string +{ + $value = trim(preg_replace('/\s+/', ' ', $value) ?? ''); + return store_substr($value, 0, $max); +} + +function store_normalize_checkout_input(array $source): array +{ + $defaults = store_checkout_defaults(); + + return [ + 'customer_name' => store_sanitize_line((string)($source['customer_name'] ?? $defaults['customer_name']), 120), + 'email' => store_lower(trim((string)($source['email'] ?? $defaults['email']))), + 'phone' => store_sanitize_line((string)($source['phone'] ?? $defaults['phone']), 40), + 'address' => trim(store_substr((string)($source['address'] ?? $defaults['address']), 0, 500)), + 'note' => trim(store_substr((string)($source['note'] ?? $defaults['note']), 0, 500)), + 'payment_method' => store_sanitize_line((string)($source['payment_method'] ?? $defaults['payment_method']), 40), + ]; +} + +function store_validate_checkout_input(array $data): array +{ + $errors = []; + $methods = store_payment_methods(); + + if (store_strlen($data['customer_name']) < 3) { + $errors['customer_name'] = 'Nama minimal 3 karakter.'; + } + + if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { + $errors['email'] = 'Masukkan email yang valid.'; + } + + $normalizedPhone = preg_replace('/[^0-9+]/', '', $data['phone']) ?? ''; + if (strlen($normalizedPhone) < 8) { + $errors['phone'] = 'Nomor telepon minimal 8 digit.'; + } + + if (store_strlen($data['address']) < 10) { + $errors['address'] = 'Alamat minimal 10 karakter.'; + } + + if (!isset($methods[$data['payment_method']])) { + $errors['payment_method'] = 'Pilih metode pembayaran yang tersedia.'; + } + + return $errors; +} + +function store_ensure_schema(): void +{ + static $ready = false; + if ($ready) { + return; + } + + db()->exec( + "CREATE TABLE IF NOT EXISTS orders ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + order_number VARCHAR(30) NOT NULL UNIQUE, + customer_name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL, + phone VARCHAR(40) NOT NULL, + address TEXT NOT NULL, + note TEXT NULL, + payment_method VARCHAR(40) NOT NULL, + status VARCHAR(40) NOT NULL DEFAULT 'Menunggu Pembayaran', + subtotal DECIMAL(12,2) NOT NULL DEFAULT 0, + shipping_fee DECIMAL(12,2) NOT NULL DEFAULT 0, + grand_total DECIMAL(12,2) NOT NULL DEFAULT 0, + items_json LONGTEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_order_number (order_number), + INDEX idx_email_created_at (email, created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + $ready = true; +} + +function store_generate_order_number(): string +{ + store_ensure_schema(); + $pdo = db(); + + for ($attempt = 0; $attempt < 6; $attempt++) { + $candidate = 'SB' . date('ymd') . '-' . (string)random_int(1000, 9999); + $stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = :order_number LIMIT 1'); + $stmt->execute(['order_number' => $candidate]); + if (!$stmt->fetch()) { + return $candidate; + } + } + + return 'SB' . date('ymdHis'); +} + +function store_create_order(array $input): array +{ + $data = store_normalize_checkout_input($input); + $errors = store_validate_checkout_input($data); + $summary = store_cart_summary(); + + if (empty($summary['lines'])) { + $errors['cart'] = 'Keranjang masih kosong. Tambahkan produk terlebih dahulu.'; + } + + if ($errors) { + return [ + 'success' => false, + 'errors' => $errors, + 'data' => $data, + ]; + } + + store_ensure_schema(); + + $items = array_map( + static function (array $line): array { + return [ + 'slug' => $line['slug'], + 'name' => $line['product']['name'], + 'price' => (float)$line['product']['price'], + 'quantity' => (int)$line['quantity'], + 'line_total' => (float)$line['line_total'], + ]; + }, + $summary['lines'] + ); + + $orderNumber = store_generate_order_number(); + $stmt = db()->prepare( + 'INSERT INTO orders ( + order_number, + customer_name, + email, + phone, + address, + note, + payment_method, + status, + subtotal, + shipping_fee, + grand_total, + items_json + ) VALUES ( + :order_number, + :customer_name, + :email, + :phone, + :address, + :note, + :payment_method, + :status, + :subtotal, + :shipping_fee, + :grand_total, + :items_json + )' + ); + + $stmt->bindValue(':order_number', $orderNumber); + $stmt->bindValue(':customer_name', $data['customer_name']); + $stmt->bindValue(':email', $data['email']); + $stmt->bindValue(':phone', $data['phone']); + $stmt->bindValue(':address', $data['address']); + $stmt->bindValue(':note', $data['note'] === '' ? null : $data['note']); + $stmt->bindValue(':payment_method', $data['payment_method']); + $stmt->bindValue(':status', 'Menunggu Pembayaran'); + $stmt->bindValue(':subtotal', $summary['subtotal']); + $stmt->bindValue(':shipping_fee', $summary['shipping_fee']); + $stmt->bindValue(':grand_total', $summary['grand_total']); + $stmt->bindValue(':items_json', json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $stmt->execute(); + + $_SESSION[STORE_CART_KEY] = []; + $_SESSION[STORE_LAST_ORDER_KEY] = [ + 'order_number' => $orderNumber, + 'email' => $data['email'], + ]; + + return [ + 'success' => true, + 'order_number' => $orderNumber, + 'data' => $data, + ]; +} + +function store_find_order(string $orderNumber, string $email = ''): ?array +{ + $orderNumber = store_sanitize_line($orderNumber, 30); + $email = trim(store_lower($email)); + + if ($orderNumber === '') { + return null; + } + + store_ensure_schema(); + + $sql = 'SELECT * FROM orders WHERE order_number = :order_number'; + $params = ['order_number' => $orderNumber]; + + if ($email !== '') { + $sql .= ' AND email = :email'; + $params['email'] = $email; + } + + $sql .= ' LIMIT 1'; + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + + $order = $stmt->fetch(); + if (!$order) { + return null; + } + + $order['items'] = json_decode((string)$order['items_json'], true) ?: []; + $paymentMethods = store_payment_methods(); + $order['payment_method_label'] = $paymentMethods[$order['payment_method']]['label'] ?? $order['payment_method']; + $order['payment_instruction'] = $paymentMethods[$order['payment_method']]['instruction'] ?? 'Tim admin akan menghubungi Anda untuk instruksi berikutnya.'; + + return $order; +} + +function store_last_order_lookup(): array +{ + $lookup = $_SESSION[STORE_LAST_ORDER_KEY] ?? []; + return is_array($lookup) ? $lookup : []; +} + +function store_safe_redirect(string $target, string $fallback = 'cart.php'): string +{ + $target = trim($target); + if ($target === '') { + return $fallback; + } + + $parts = parse_url($target); + if ($parts === false || isset($parts['scheme']) || isset($parts['host'])) { + return $fallback; + } + + $path = $parts['path'] ?? ''; + if ($path === '' || str_contains($path, '..')) { + return $fallback; + } + + $path = ltrim($path, '/'); + if ($path === '') { + $path = $fallback; + } + + $query = isset($parts['query']) ? '?' . $parts['query'] : ''; + + return $path . $query; +} + +function store_format_datetime(string $value): string +{ + $timestamp = strtotime($value); + if ($timestamp === false) { + return $value; + } + + $months = [ + 1 => 'Jan', + 2 => 'Feb', + 3 => 'Mar', + 4 => 'Apr', + 5 => 'Mei', + 6 => 'Jun', + 7 => 'Jul', + 8 => 'Agu', + 9 => 'Sep', + 10 => 'Okt', + 11 => 'Nov', + 12 => 'Des', + ]; + + $month = $months[(int)date('n', $timestamp)] ?? date('M', $timestamp); + return date('d', $timestamp) . ' ' . $month . ' ' . date('Y, H:i', $timestamp); +} + +function store_nav_link(string $href, string $label, string $currentPath): string +{ + $active = $currentPath === $href ? ' nav-link active' : ' nav-link'; + return ''; +} + +function store_page_start(string $title, string $description = '', array $options = []): void +{ + $projectName = app_env('PROJECT_NAME', store_brand()); + $projectDescription = app_env('PROJECT_DESCRIPTION', 'Toko online bakery dengan katalog, keranjang, checkout, dan halaman status pesanan.'); + $projectImageUrl = app_env('PROJECT_IMAGE_URL', ''); + $metaDescription = $description !== '' ? $description : $projectDescription; + $fullTitle = trim($title) !== '' ? $title . ' • ' . $projectName : $projectName; + $cssVersion = file_exists(__DIR__ . '/assets/css/custom.css') ? (string)filemtime(__DIR__ . '/assets/css/custom.css') : (string)time(); + $currentPath = basename(parse_url($_SERVER['REQUEST_URI'] ?? '/index.php', PHP_URL_PATH) ?: 'index.php'); + if ($currentPath === '' || $currentPath === '/') { + $currentPath = 'index.php'; + } + $robots = !empty($options['noindex']) ? '' : ''; + $cartCount = store_cart_count(); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '' . h($fullTitle) . ''; + echo $robots; + + if ($metaDescription !== '') { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + if ($projectImageUrl !== '') { + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + $flashes = store_consume_flashes(); + if ($flashes) { + echo '
'; + foreach ($flashes as $index => $flash) { + $type = $flash['type'] ?? 'info'; + $class = match ($type) { + 'success' => 'toast-theme toast-theme--success', + 'warning' => 'toast-theme toast-theme--warning', + 'danger' => 'toast-theme toast-theme--danger', + default => 'toast-theme', + }; + echo ''; + } + echo '
'; + } + + echo '
'; + echo '
'; +} + +function store_page_end(): void +{ + $jsVersion = file_exists(__DIR__ . '/assets/js/main.js') ? (string)filemtime(__DIR__ . '/assets/js/main.js') : (string)time(); + + echo '
'; + echo '
'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; +}