diff --git a/admin/css/admin.css b/admin/css/admin.css
new file mode 100644
index 0000000..7753537
--- /dev/null
+++ b/admin/css/admin.css
@@ -0,0 +1,974 @@
+/* ============================================
+ Grubflip Admin Dashboard โ Core Styles
+ Mobile-first responsive design
+ ============================================ */
+
+/* --- Reset & Base --- */
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-size: 16px;
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ font-family: var(--gf-font-body);
+ font-size: var(--gf-text-base);
+ color: var(--gf-neutral-800);
+ background: var(--gf-bg-subtle);
+ line-height: 1.5;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: var(--gf-font-heading);
+ font-weight: var(--gf-weight-bold);
+ line-height: 1.2;
+ color: var(--gf-neutral-900);
+}
+
+a { color: var(--gf-primary); text-decoration: none; }
+a:hover { color: var(--gf-primary-dark); }
+
+img { max-width: 100%; height: auto; }
+
+/* --- Layout Shell --- */
+.admin-layout {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* --- Sidebar --- */
+.sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: var(--gf-sidebar-width);
+ height: 100vh;
+ background: var(--gf-bg-dark);
+ color: var(--gf-neutral-300);
+ display: flex;
+ flex-direction: column;
+ z-index: var(--gf-z-fixed);
+ transform: translateX(-100%);
+ transition: transform var(--gf-duration-slow) var(--gf-ease);
+ overflow-y: auto;
+}
+
+.sidebar.open {
+ transform: translateX(0);
+}
+
+.sidebar-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.5);
+ z-index: calc(var(--gf-z-fixed) - 1);
+}
+
+.sidebar-overlay.visible {
+ display: block;
+}
+
+.sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: var(--gf-space-3);
+ padding: var(--gf-space-6) var(--gf-space-5);
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+}
+
+.sidebar-brand .logo-icon {
+ width: 2rem;
+ height: 2rem;
+ background: var(--gf-primary);
+ border-radius: var(--gf-radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.1rem;
+ color: #fff;
+ font-weight: var(--gf-weight-bold);
+ flex-shrink: 0;
+}
+
+.sidebar-brand span {
+ font-family: var(--gf-font-heading);
+ font-size: var(--gf-text-lg);
+ font-weight: var(--gf-weight-bold);
+ color: #fff;
+}
+
+.sidebar-nav {
+ flex: 1;
+ padding: var(--gf-space-4) 0;
+}
+
+.sidebar-section-title {
+ font-size: var(--gf-text-xs);
+ font-weight: var(--gf-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--gf-neutral-500);
+ padding: var(--gf-space-4) var(--gf-space-5) var(--gf-space-2);
+}
+
+.sidebar-link {
+ display: flex;
+ align-items: center;
+ gap: var(--gf-space-3);
+ padding: var(--gf-space-3) var(--gf-space-5);
+ color: var(--gf-neutral-300);
+ font-size: var(--gf-text-sm);
+ font-weight: var(--gf-weight-medium);
+ transition: all var(--gf-duration-fast) var(--gf-ease);
+ cursor: pointer;
+ border: none;
+ background: none;
+ width: 100%;
+ text-align: left;
+}
+
+.sidebar-link:hover {
+ background: rgba(255,255,255,0.05);
+ color: #fff;
+}
+
+.sidebar-link.active {
+ background: rgba(255,107,53,0.15);
+ color: var(--gf-primary);
+}
+
+.sidebar-link .icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1rem;
+}
+
+.sidebar-footer {
+ padding: var(--gf-space-4) var(--gf-space-5);
+ border-top: 1px solid rgba(255,255,255,0.08);
+}
+
+.sidebar-user {
+ display: flex;
+ align-items: center;
+ gap: var(--gf-space-3);
+}
+
+.sidebar-user .avatar {
+ width: 2rem;
+ height: 2rem;
+ border-radius: var(--gf-radius-full);
+ background: var(--gf-primary);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--gf-text-xs);
+ font-weight: var(--gf-weight-semibold);
+ flex-shrink: 0;
+}
+
+.sidebar-user .user-info {
+ overflow: hidden;
+}
+
+.sidebar-user .user-name {
+ font-size: var(--gf-text-sm);
+ font-weight: var(--gf-weight-medium);
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sidebar-user .user-role {
+ font-size: var(--gf-text-xs);
+ color: var(--gf-neutral-400);
+}
+
+/* --- Main Content --- */
+.main-content {
+ flex: 1;
+ width: 100%;
+ min-height: 100vh;
+}
+
+/* --- Top Bar --- */
+.topbar {
+ position: sticky;
+ top: 0;
+ background: var(--gf-bg);
+ height: var(--gf-navbar-height);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 var(--gf-space-4);
+ border-bottom: 1px solid var(--gf-neutral-200);
+ z-index: var(--gf-z-sticky);
+}
+
+.topbar-left {
+ display: flex;
+ align-items: center;
+ gap: var(--gf-space-3);
+}
+
+.menu-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ height: 2.5rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: var(--gf-radius-md);
+ color: var(--gf-neutral-600);
+ font-size: 1.25rem;
+ transition: background var(--gf-duration-fast);
+}
+
+.menu-toggle:hover {
+ background: var(--gf-neutral-100);
+}
+
+.topbar-title {
+ font-family: var(--gf-font-heading);
+ font-size: var(--gf-text-lg);
+ font-weight: var(--gf-weight-semibold);
+}
+
+.topbar-right {
+ display: flex;
+ align-items: center;
+ gap: var(--gf-space-2);
+}
+
+.topbar-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ height: 2.5rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: var(--gf-radius-full);
+ color: var(--gf-neutral-600);
+ font-size: 1.1rem;
+ position: relative;
+ transition: background var(--gf-duration-fast);
+}
+
+.topbar-btn:hover {
+ background: var(--gf-neutral-100);
+}
+
+.topbar-btn .badge-dot {
+ position: absolute;
+ top: 0.4rem;
+ right: 0.4rem;
+ width: 0.5rem;
+ height: 0.5rem;
+ background: var(--gf-error);
+ border-radius: var(--gf-radius-full);
+ border: 2px solid var(--gf-bg);
+}
+
+/* --- Page Content --- */
+.page-content {
+ padding: var(--gf-space-4);
+}
+
+.page-header {
+ margin-bottom: var(--gf-space-6);
+}
+
+.page-header h1 {
+ font-size: var(--gf-text-2xl);
+ margin-bottom: var(--gf-space-1);
+}
+
+.page-header .subtitle {
+ color: var(--gf-neutral-500);
+ font-size: var(--gf-text-sm);
+}
+
+/* --- Cards --- */
+.card {
+ background: var(--gf-bg);
+ border-radius: var(--gf-radius-lg);
+ box-shadow: var(--gf-shadow-card);
+ border: 1px solid var(--gf-neutral-200);
+ padding: var(--gf-space-6);
+ transition: box-shadow var(--gf-duration-normal) var(--gf-ease);
+}
+
+.card:hover {
+ box-shadow: 0 4px 12px rgba(0,0,0,0.12);
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: var(--gf-space-4);
+}
+
+.card-header h2 {
+ font-size: var(--gf-text-lg);
+ font-weight: var(--gf-weight-semibold);
+}
+
+/* --- Stat Cards --- */
+.stats-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--gf-space-4);
+ margin-bottom: var(--gf-space-6);
+}
+
+.stat-card {
+ background: var(--gf-bg);
+ border-radius: var(--gf-radius-lg);
+ box-shadow: var(--gf-shadow-card);
+ border: 1px solid var(--gf-neutral-200);
+ padding: var(--gf-space-5);
+ display: flex;
+ align-items: flex-start;
+ gap: var(--gf-space-4);
+}
+
+.stat-icon {
+ width: 3rem;
+ height: 3rem;
+ border-radius: var(--gf-radius-lg);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.25rem;
+ flex-shrink: 0;
+}
+
+.stat-icon.orange { background: var(--gf-primary-bg); color: var(--gf-primary); }
+.stat-icon.green { background: var(--gf-secondary-bg); color: var(--gf-secondary); }
+.stat-icon.yellow { background: var(--gf-accent-bg); color: var(--gf-accent-dark); }
+.stat-icon.blue { background: var(--gf-info-bg); color: var(--gf-info); }
+
+.stat-info h3 {
+ font-size: var(--gf-text-xs);
+ font-weight: var(--gf-weight-medium);
+ color: var(--gf-neutral-500);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: var(--gf-space-1);
+}
+
+.stat-info .stat-value {
+ font-family: var(--gf-font-heading);
+ font-size: var(--gf-text-2xl);
+ font-weight: var(--gf-weight-bold);
+ color: var(--gf-neutral-900);
+}
+
+.stat-info .stat-change {
+ font-size: var(--gf-text-xs);
+ margin-top: var(--gf-space-1);
+}
+
+.stat-change.up { color: var(--gf-success); }
+.stat-change.down { color: var(--gf-error); }
+
+/* --- Buttons --- */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--gf-space-2);
+ font-family: var(--gf-font-body);
+ font-size: var(--gf-text-base);
+ font-weight: var(--gf-weight-semibold);
+ padding: 0.75rem 1.5rem;
+ border-radius: var(--gf-radius-md);
+ border: none;
+ cursor: pointer;
+ transition: all var(--gf-duration-normal) var(--gf-ease);
+ white-space: nowrap;
+ line-height: 1;
+}
+
+.btn-sm {
+ padding: 0.5rem 1rem;
+ font-size: var(--gf-text-sm);
+}
+
+.btn-primary {
+ background: var(--gf-primary);
+ color: #fff;
+}
+
+.btn-primary:hover { background: var(--gf-primary-dark); }
+.btn-primary:disabled { background: #FFB899; cursor: not-allowed; }
+
+.btn-secondary {
+ background: transparent;
+ color: var(--gf-primary);
+ border: 2px solid var(--gf-primary);
+}
+
+.btn-secondary:hover {
+ background: var(--gf-primary-bg);
+ color: var(--gf-primary-dark);
+ border-color: var(--gf-primary-dark);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--gf-neutral-500);
+ padding: 0.5rem 1rem;
+ font-size: var(--gf-text-sm);
+ font-weight: var(--gf-weight-medium);
+}
+
+.btn-ghost:hover {
+ background: var(--gf-neutral-100);
+ color: var(--gf-neutral-700);
+}
+
+.btn-danger {
+ background: var(--gf-error);
+ color: #fff;
+}
+
+.btn-danger:hover { background: #c0392b; }
+
+.btn-success {
+ background: var(--gf-success);
+ color: #fff;
+}
+
+.btn-success:hover { background: #27ae60; }
+
+/* --- Forms --- */
+.form-group {
+ margin-bottom: var(--gf-space-4);
+}
+
+.form-label {
+ display: block;
+ font-size: var(--gf-text-sm);
+ font-weight: var(--gf-weight-medium);
+ color: var(--gf-neutral-700);
+ margin-bottom: var(--gf-space-2);
+}
+
+.form-input,
+.form-select,
+.form-textarea {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ font-family: var(--gf-font-body);
+ font-size: var(--gf-text-base);
+ color: var(--gf-neutral-800);
+ background: var(--gf-bg);
+ border: 1px solid var(--gf-neutral-300);
+ border-radius: var(--gf-radius-md);
+ transition: border-color var(--gf-duration-normal), box-shadow var(--gf-duration-normal);
+ line-height: 1.5;
+}
+
+.form-input::placeholder,
+.form-textarea::placeholder {
+ color: var(--gf-neutral-400);
+}
+
+.form-input:focus,
+.form-select:focus,
+.form-textarea:focus {
+ outline: none;
+ border-color: var(--gf-primary);
+ box-shadow: 0 0 0 3px rgba(255,107,53,0.15);
+}
+
+.form-input.error {
+ border-color: var(--gf-error);
+ box-shadow: 0 0 0 3px rgba(231,76,60,0.15);
+}
+
+.form-error {
+ font-size: var(--gf-text-xs);
+ color: var(--gf-error);
+ margin-top: var(--gf-space-1);
+}
+
+.form-hint {
+ font-size: var(--gf-text-xs);
+ color: var(--gf-neutral-400);
+ margin-top: var(--gf-space-1);
+}
+
+.form-textarea {
+ resize: vertical;
+ min-height: 6rem;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--gf-space-4);
+}
+
+/* --- Badges --- */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.75rem;
+ font-size: var(--gf-text-xs);
+ font-weight: var(--gf-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ border-radius: var(--gf-radius-full);
+}
+
+.badge-default { background: var(--gf-neutral-100); color: var(--gf-neutral-600); }
+.badge-primary { background: var(--gf-primary-bg); color: var(--gf-primary-dark); }
+.badge-success { background: var(--gf-success-bg); color: #1B8A4A; }
+.badge-warning { background: var(--gf-warning-bg); color: #B87A0D; }
+.badge-error { background: var(--gf-error-bg); color: #C0392B; }
+.badge-info { background: var(--gf-info-bg); color: #2471A3; }
+.badge-accent { background: var(--gf-accent-bg); color: #B8860B; }
+
+/* --- Tables --- */
+.table-wrapper {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: var(--gf-text-sm);
+}
+
+.data-table thead {
+ background: var(--gf-neutral-100);
+}
+
+.data-table th {
+ text-align: left;
+ padding: 0.75rem 1rem;
+ font-size: var(--gf-text-xs);
+ font-weight: var(--gf-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--gf-neutral-600);
+ white-space: nowrap;
+}
+
+.data-table td {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--gf-neutral-200);
+ color: var(--gf-neutral-700);
+}
+
+.data-table tbody tr:hover {
+ background: var(--gf-primary-bg);
+}
+
+.data-table .actions {
+ display: flex;
+ gap: var(--gf-space-2);
+}
+
+/* --- Modal --- */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.5);
+ z-index: var(--gf-z-modal-backdrop);
+ align-items: center;
+ justify-content: center;
+ padding: var(--gf-space-4);
+}
+
+.modal-overlay.visible {
+ display: flex;
+}
+
+.modal {
+ background: var(--gf-bg);
+ border-radius: var(--gf-radius-xl);
+ box-shadow: var(--gf-shadow-xl);
+ width: 100%;
+ max-width: 32rem;
+ max-height: 90vh;
+ overflow-y: auto;
+ animation: modalIn var(--gf-duration-slow) var(--gf-ease-bounce);
+}
+
+@keyframes modalIn {
+ from { opacity: 0; transform: scale(0.95) translateY(1rem); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--gf-space-6) var(--gf-space-6) 0;
+}
+
+.modal-header h2 {
+ font-size: var(--gf-text-xl);
+ font-weight: var(--gf-weight-bold);
+}
+
+.modal-close {
+ width: 2rem;
+ height: 2rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: var(--gf-radius-full);
+ color: var(--gf-neutral-400);
+ font-size: 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background var(--gf-duration-fast);
+}
+
+.modal-close:hover {
+ background: var(--gf-neutral-100);
+ color: var(--gf-neutral-700);
+}
+
+.modal-body {
+ padding: var(--gf-space-6);
+}
+
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--gf-space-3);
+ padding: 0 var(--gf-space-6) var(--gf-space-6);
+}
+
+/* --- Toast Notifications --- */
+.toast-container {
+ position: fixed;
+ top: var(--gf-space-4);
+ right: var(--gf-space-4);
+ z-index: var(--gf-z-toast);
+ display: flex;
+ flex-direction: column;
+ gap: var(--gf-space-3);
+ max-width: 24rem;
+ width: calc(100% - 2rem);
+}
+
+.toast {
+ padding: 1rem 1.5rem;
+ border-radius: var(--gf-radius-md);
+ box-shadow: var(--gf-shadow-lg);
+ font-size: var(--gf-text-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--gf-space-3);
+ animation: toastIn var(--gf-duration-slow) var(--gf-ease-bounce);
+}
+
+.toast.removing {
+ animation: toastOut var(--gf-duration-normal) var(--gf-ease) forwards;
+}
+
+@keyframes toastIn {
+ from { opacity: 0; transform: translateX(1rem); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes toastOut {
+ to { opacity: 0; transform: translateX(1rem); }
+}
+
+.toast-success { background: var(--gf-success-bg); border: 1px solid var(--gf-success); }
+.toast-error { background: var(--gf-error-bg); border: 1px solid var(--gf-error); }
+.toast-warning { background: var(--gf-warning-bg); border: 1px solid var(--gf-warning); }
+.toast-info { background: var(--gf-info-bg); border: 1px solid var(--gf-info); }
+
+/* --- Empty State --- */
+.empty-state {
+ text-align: center;
+ padding: var(--gf-space-12) var(--gf-space-6);
+ color: var(--gf-neutral-400);
+}
+
+.empty-state .icon {
+ font-size: 3rem;
+ margin-bottom: var(--gf-space-4);
+}
+
+.empty-state h3 {
+ font-size: var(--gf-text-lg);
+ color: var(--gf-neutral-600);
+ margin-bottom: var(--gf-space-2);
+}
+
+.empty-state p {
+ font-size: var(--gf-text-sm);
+ margin-bottom: var(--gf-space-6);
+}
+
+/* --- Loading Spinner --- */
+.spinner {
+ width: 2rem;
+ height: 2rem;
+ border: 3px solid var(--gf-neutral-200);
+ border-top-color: var(--gf-primary);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.loading-overlay {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--gf-space-12);
+}
+
+/* --- Tabs --- */
+.tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 2px solid var(--gf-neutral-200);
+ margin-bottom: var(--gf-space-6);
+ overflow-x: auto;
+}
+
+.tab {
+ padding: var(--gf-space-3) var(--gf-space-4);
+ font-size: var(--gf-text-sm);
+ font-weight: var(--gf-weight-medium);
+ color: var(--gf-neutral-500);
+ border: none;
+ background: none;
+ cursor: pointer;
+ white-space: nowrap;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ transition: all var(--gf-duration-fast);
+}
+
+.tab:hover { color: var(--gf-neutral-700); }
+
+.tab.active {
+ color: var(--gf-primary);
+ border-bottom-color: var(--gf-primary);
+}
+
+/* --- Search --- */
+.search-box {
+ position: relative;
+}
+
+.search-box .search-icon {
+ position: absolute;
+ left: 0.75rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--gf-neutral-400);
+ font-size: 1rem;
+ pointer-events: none;
+}
+
+.search-box .form-input {
+ padding-left: 2.5rem;
+}
+
+/* --- Toggle Switch --- */
+.toggle {
+ position: relative;
+ display: inline-block;
+ width: 2.75rem;
+ height: 1.5rem;
+}
+
+.toggle input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.toggle-slider {
+ position: absolute;
+ inset: 0;
+ background: var(--gf-neutral-300);
+ border-radius: var(--gf-radius-full);
+ cursor: pointer;
+ transition: background var(--gf-duration-normal);
+}
+
+.toggle-slider::before {
+ content: '';
+ position: absolute;
+ width: 1.125rem;
+ height: 1.125rem;
+ left: 0.1875rem;
+ bottom: 0.1875rem;
+ background: #fff;
+ border-radius: 50%;
+ transition: transform var(--gf-duration-normal) var(--gf-ease);
+}
+
+.toggle input:checked + .toggle-slider {
+ background: var(--gf-primary);
+}
+
+.toggle input:checked + .toggle-slider::before {
+ transform: translateX(1.25rem);
+}
+
+/* --- Chip / Tag --- */
+.chip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--gf-space-1);
+ padding: 0.375rem 0.75rem;
+ background: var(--gf-neutral-100);
+ color: var(--gf-neutral-600);
+ font-size: var(--gf-text-sm);
+ font-weight: var(--gf-weight-medium);
+ border-radius: var(--gf-radius-full);
+ border: none;
+ cursor: default;
+}
+
+.chip-remove {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ height: 1rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ color: var(--gf-neutral-400);
+ font-size: 0.75rem;
+ border-radius: 50%;
+ transition: all var(--gf-duration-fast);
+}
+
+.chip-remove:hover {
+ background: var(--gf-neutral-200);
+ color: var(--gf-neutral-700);
+}
+
+/* --- Utility Classes --- */
+.text-center { text-align: center; }
+.text-right { text-align: right; }
+.text-muted { color: var(--gf-neutral-500); }
+.text-sm { font-size: var(--gf-text-sm); }
+.text-xs { font-size: var(--gf-text-xs); }
+.font-mono { font-family: var(--gf-font-mono); }
+.mt-2 { margin-top: var(--gf-space-2); }
+.mt-4 { margin-top: var(--gf-space-4); }
+.mt-6 { margin-top: var(--gf-space-6); }
+.mb-2 { margin-bottom: var(--gf-space-2); }
+.mb-4 { margin-bottom: var(--gf-space-4); }
+.mb-6 { margin-bottom: var(--gf-space-6); }
+.flex { display: flex; }
+.flex-col { flex-direction: column; }
+.items-center { align-items: center; }
+.justify-between { justify-content: space-between; }
+.gap-2 { gap: var(--gf-space-2); }
+.gap-3 { gap: var(--gf-space-3); }
+.gap-4 { gap: var(--gf-space-4); }
+.hidden { display: none !important; }
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* ==========================================
+ RESPONSIVE BREAKPOINTS
+ ========================================== */
+
+/* sm: 640px+ */
+@media (min-width: 640px) {
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .form-row {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+/* md: 768px+ */
+@media (min-width: 768px) {
+ .page-content {
+ padding: var(--gf-space-6);
+ }
+
+ .page-header h1 {
+ font-size: var(--gf-text-3xl);
+ }
+}
+
+/* lg: 1024px+ โ sidebar always visible */
+@media (min-width: 1024px) {
+ .sidebar {
+ transform: translateX(0);
+ }
+
+ .sidebar-overlay {
+ display: none !important;
+ }
+
+ .menu-toggle {
+ display: none;
+ }
+
+ .main-content {
+ margin-left: var(--gf-sidebar-width);
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .page-content {
+ padding: var(--gf-space-8);
+ }
+}
+
+/* xl: 1280px+ */
+@media (min-width: 1280px) {
+ .page-content {
+ max-width: 80rem;
+ }
+}
diff --git a/admin/css/tokens.css b/admin/css/tokens.css
new file mode 100644
index 0000000..9a4b78d
--- /dev/null
+++ b/admin/css/tokens.css
@@ -0,0 +1,122 @@
+/* ============================================
+ Grubflip Design Tokens โ CSS Custom Properties
+ Generated from brand-tokens v2.0.0
+ ============================================ */
+
+:root {
+ /* --- Colors: Primary (Warm Orange) --- */
+ --gf-primary: #FF6B35;
+ --gf-primary-light: #FF8F66;
+ --gf-primary-dark: #D94E1F;
+ --gf-primary-bg: #FFF3ED;
+
+ /* --- Colors: Secondary (Deep Green) --- */
+ --gf-secondary: #2D6A4F;
+ --gf-secondary-light: #40916C;
+ --gf-secondary-dark: #1B4332;
+ --gf-secondary-bg: #EDF5F0;
+
+ /* --- Colors: Accent (Golden Yellow) --- */
+ --gf-accent: #FFBA08;
+ --gf-accent-light: #FFD166;
+ --gf-accent-dark: #E0A100;
+ --gf-accent-bg: #FFF8E1;
+
+ /* --- Semantic Colors --- */
+ --gf-success: #2ECC71;
+ --gf-success-bg: #EAFAF1;
+ --gf-warning: #F39C12;
+ --gf-warning-bg: #FEF5E7;
+ --gf-error: #E74C3C;
+ --gf-error-bg: #FDEDEC;
+ --gf-info: #3498DB;
+ --gf-info-bg: #EBF5FB;
+
+ /* --- Neutrals --- */
+ --gf-neutral-50: #FAFAFA;
+ --gf-neutral-100: #F5F5F5;
+ --gf-neutral-200: #E5E5E5;
+ --gf-neutral-300: #D4D4D4;
+ --gf-neutral-400: #A3A3A3;
+ --gf-neutral-500: #737373;
+ --gf-neutral-600: #525252;
+ --gf-neutral-700: #404040;
+ --gf-neutral-800: #262626;
+ --gf-neutral-900: #171717;
+
+ /* --- Backgrounds --- */
+ --gf-bg: #FFFFFF;
+ --gf-bg-subtle: #FAFAFA;
+ --gf-bg-muted: #F5F5F5;
+ --gf-bg-dark: #1A1A2E;
+
+ /* --- Typography --- */
+ --gf-font-heading: 'Plus Jakarta Sans', sans-serif;
+ --gf-font-body: 'Inter', sans-serif;
+ --gf-font-mono: 'JetBrains Mono', monospace;
+
+ --gf-text-xs: 0.75rem;
+ --gf-text-sm: 0.875rem;
+ --gf-text-base: 1rem;
+ --gf-text-lg: 1.125rem;
+ --gf-text-xl: 1.25rem;
+ --gf-text-2xl: 1.5rem;
+ --gf-text-3xl: 1.875rem;
+ --gf-text-4xl: 2.25rem;
+
+ --gf-weight-regular: 400;
+ --gf-weight-medium: 500;
+ --gf-weight-semibold: 600;
+ --gf-weight-bold: 700;
+ --gf-weight-extrabold: 800;
+
+ /* --- Spacing --- */
+ --gf-space-0: 0;
+ --gf-space-1: 0.25rem;
+ --gf-space-2: 0.5rem;
+ --gf-space-3: 0.75rem;
+ --gf-space-4: 1rem;
+ --gf-space-5: 1.25rem;
+ --gf-space-6: 1.5rem;
+ --gf-space-8: 2rem;
+ --gf-space-10: 2.5rem;
+ --gf-space-12: 3rem;
+ --gf-space-16: 4rem;
+ --gf-space-20: 5rem;
+
+ /* --- Border Radius --- */
+ --gf-radius-sm: 0.375rem;
+ --gf-radius-md: 0.5rem;
+ --gf-radius-lg: 0.75rem;
+ --gf-radius-xl: 1rem;
+ --gf-radius-2xl: 1.5rem;
+ --gf-radius-full: 9999px;
+
+ /* --- Shadows --- */
+ --gf-shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+ --gf-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
+ --gf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
+ --gf-shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
+ --gf-shadow-card: 0 2px 8px rgba(0,0,0,0.08);
+
+ /* --- Animation --- */
+ --gf-duration-fast: 150ms;
+ --gf-duration-normal: 200ms;
+ --gf-duration-slow: 300ms;
+ --gf-ease: cubic-bezier(0.4, 0, 0.2, 1);
+ --gf-ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --gf-ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
+
+ /* --- Z-Index --- */
+ --gf-z-dropdown: 1000;
+ --gf-z-sticky: 1020;
+ --gf-z-fixed: 1030;
+ --gf-z-modal-backdrop: 1040;
+ --gf-z-modal: 1050;
+ --gf-z-toast: 1080;
+
+ /* --- Sidebar --- */
+ --gf-sidebar-width: 16rem;
+ --gf-sidebar-collapsed: 4.5rem;
+ --gf-navbar-height: 4rem;
+}
diff --git a/admin/index.html b/admin/index.html
new file mode 100644
index 0000000..53e9e81
--- /dev/null
+++ b/admin/index.html
@@ -0,0 +1,194 @@
+
+
+
+
+
+ Grubflip Admin โ Login
+
+
+
+
+
+
+
+
+
+
+
+
๐
+
Grubflip Admin
+
Restaurant management dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chat.html b/chat.html
new file mode 100644
index 0000000..e774586
--- /dev/null
+++ b/chat.html
@@ -0,0 +1,173 @@
+
+
+
+
+
+ GrubFlip โ Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ๐
+ Margherita Pizza
+ You
+
+
+
+ ๐
+ Pad Thai
+ Marcus
+
+
+
+ Accepted
+ Expires in 2h 15m
+
+
+ Set Meetup
+ Confirm Trade
+
+
+
+
+
+
+
+
+ Today
+
+
+
+
+
+ Trade request sent for Margherita Pizza โ Pad Thai
+
+
+
+
+
+
+
Hey! Your margherita looks amazing ๐คค I just made a fresh batch of pad thai โ extra peanuts and lime. Interested in a swap?
+
4:32 PM
+
+
+
+
+
+
+
Absolutely! I just pulled it out of the wood-fired oven. Still warm ๐ฅ
+
4:34 PM
+
+
+
+
+
+
+ Marcus accepted the trade!
+
+
+
+
+
+
+
Awesome! Where do you want to meet? I'm near the downtown area
+
4:36 PM
+
+
+
+
+
+
+
I'm at Rosario's on 5th. Can you swing by around 6?
+
4:37 PM
+
+
+
+
+
+
+
+
Sounds good! I can meet at the corner of 5th and Main around 6pm ๐
+
4:38 PM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Share Location
+
+
+
+ Suggest Meetup Time
+
+
+
+ Complete Trade
+
+
+
+
+
diff --git a/css/chat.css b/css/chat.css
new file mode 100644
index 0000000..baad62f
--- /dev/null
+++ b/css/chat.css
@@ -0,0 +1,974 @@
+/* ============================================
+ GrubFlip โ Chat & Messages Styles
+ Mobile-first responsive design
+ Uses brand tokens from styles.css
+ ============================================ */
+
+/* --- Messages Inbox Page --- */
+
+.messages-inbox {
+ padding-top: 0.5rem;
+ padding-bottom: 5rem; /* space for bottom nav */
+}
+
+.chat-search {
+ position: relative;
+ margin: 0.75rem 1rem;
+}
+
+.chat-search__icon {
+ position: absolute;
+ left: 0.875rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--gf-neutral-400);
+ pointer-events: none;
+}
+
+.chat-search__input {
+ width: 100%;
+ padding: 0.625rem 0.875rem 0.625rem 2.5rem;
+ border: 1.5px solid var(--gf-neutral-200);
+ border-radius: var(--gf-radius-full);
+ background: var(--gf-bg-muted);
+ font-family: var(--gf-font-body);
+ font-size: 0.875rem;
+ color: var(--gf-neutral-900);
+ transition: border-color 0.15s ease, background 0.15s ease;
+}
+
+.chat-search__input:focus {
+ outline: none;
+ border-color: var(--gf-primary);
+ background: var(--gf-bg);
+ box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
+}
+
+.chat-search__input::placeholder {
+ color: var(--gf-neutral-400);
+}
+
+/* Trade Context Banner */
+.trade-context-banner {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0 1rem 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: var(--gf-primary-bg);
+ border-radius: var(--gf-radius-md);
+ font-family: var(--gf-font-body);
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--gf-primary-dark);
+}
+
+.trade-context-banner svg {
+ color: var(--gf-primary);
+ flex-shrink: 0;
+}
+
+/* Conversation List */
+.conversation-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.conversation-item {
+ border-bottom: 1px solid var(--gf-neutral-100);
+}
+
+.conversation-item:last-child {
+ border-bottom: none;
+}
+
+.conversation-item__link {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ padding: 0.875rem 1rem;
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.12s ease;
+}
+
+.conversation-item__link:active {
+ background: var(--gf-neutral-50);
+}
+
+/* Avatar */
+.conversation-item__avatar {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.conversation-item__avatar img {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--gf-radius-full);
+ object-fit: cover;
+ background: var(--gf-neutral-200);
+}
+
+.conversation-item__online {
+ position: absolute;
+ bottom: 1px;
+ right: 1px;
+ width: 12px;
+ height: 12px;
+ background: var(--gf-success);
+ border: 2px solid var(--gf-bg);
+ border-radius: var(--gf-radius-full);
+}
+
+/* Content */
+.conversation-item__content {
+ flex: 1;
+ min-width: 0;
+}
+
+.conversation-item__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ margin-bottom: 0.125rem;
+}
+
+.conversation-item__name {
+ font-family: var(--gf-font-heading);
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: var(--gf-neutral-900);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.conversation-item--unread .conversation-item__name {
+ color: var(--gf-neutral-900);
+}
+
+.conversation-item__time {
+ font-family: var(--gf-font-body);
+ font-size: 0.75rem;
+ color: var(--gf-neutral-400);
+ flex-shrink: 0;
+ margin-left: 0.5rem;
+}
+
+.conversation-item--unread .conversation-item__time {
+ color: var(--gf-primary);
+ font-weight: 600;
+}
+
+.conversation-item__preview {
+ font-family: var(--gf-font-body);
+ font-size: 0.8125rem;
+ color: var(--gf-neutral-500);
+ line-height: 1.4;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 0.25rem;
+}
+
+.conversation-item--unread .conversation-item__preview {
+ color: var(--gf-neutral-700);
+ font-weight: 500;
+}
+
+.conversation-item__meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.conversation-item__trade-tag {
+ font-family: var(--gf-font-body);
+ font-size: 0.6875rem;
+ color: var(--gf-neutral-500);
+ background: var(--gf-neutral-100);
+ padding: 0.125rem 0.5rem;
+ border-radius: var(--gf-radius-full);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 200px;
+}
+
+.conversation-item__trade-tag--complete {
+ background: var(--gf-success-bg);
+ color: #1a8a4a;
+}
+
+.conversation-item__trade-tag--expired {
+ background: var(--gf-neutral-100);
+ color: var(--gf-neutral-400);
+}
+
+.conversation-item__badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 0.375rem;
+ background: var(--gf-primary);
+ color: #fff;
+ font-family: var(--gf-font-body);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ border-radius: var(--gf-radius-full);
+ flex-shrink: 0;
+}
+
+
+/* ============================================
+ Chat View (Individual Conversation)
+ ============================================ */
+
+.chat-view {
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ height: 100vh; /* fallback */
+ overflow: hidden;
+}
+
+/* Chat Header */
+.chat-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: var(--gf-bg);
+ border-bottom: 1px solid var(--gf-neutral-100);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ min-height: 56px;
+}
+
+.chat-header__back {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ color: var(--gf-neutral-700);
+ text-decoration: none;
+ border-radius: var(--gf-radius-full);
+ transition: background 0.12s ease;
+ flex-shrink: 0;
+}
+
+.chat-header__back:active {
+ background: var(--gf-neutral-100);
+}
+
+.chat-header__user {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.chat-header__avatar {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.chat-header__avatar img {
+ width: 36px;
+ height: 36px;
+ border-radius: var(--gf-radius-full);
+ object-fit: cover;
+ background: var(--gf-neutral-200);
+}
+
+.chat-header__online {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 10px;
+ height: 10px;
+ background: var(--gf-success);
+ border: 2px solid var(--gf-bg);
+ border-radius: var(--gf-radius-full);
+}
+
+.chat-header__info {
+ min-width: 0;
+}
+
+.chat-header__name {
+ font-family: var(--gf-font-heading);
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: var(--gf-neutral-900);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.chat-header__status {
+ font-family: var(--gf-font-body);
+ font-size: 0.6875rem;
+ color: var(--gf-neutral-400);
+}
+
+/* Icon button (shared) */
+.btn-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ background: none;
+ border: none;
+ color: var(--gf-neutral-600);
+ border-radius: var(--gf-radius-full);
+ cursor: pointer;
+ transition: background 0.12s ease;
+ flex-shrink: 0;
+}
+
+.btn-icon:active {
+ background: var(--gf-neutral-100);
+}
+
+/* Trade Context Card */
+.trade-context-card {
+ background: var(--gf-bg);
+ border-bottom: 1px solid var(--gf-neutral-100);
+ padding: 0.625rem 1rem;
+}
+
+.trade-context-card__meals {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.trade-context-card__meal {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+ background: var(--gf-neutral-50);
+ padding: 0.375rem 0.625rem;
+ border-radius: var(--gf-radius-lg);
+ flex: 1;
+ min-width: 0;
+}
+
+.trade-context-card__emoji {
+ font-size: 1.25rem;
+ flex-shrink: 0;
+}
+
+.trade-context-card__label {
+ font-family: var(--gf-font-body);
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--gf-neutral-800);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.trade-context-card__owner {
+ font-family: var(--gf-font-body);
+ font-size: 0.625rem;
+ color: var(--gf-neutral-400);
+ margin-left: auto;
+ flex-shrink: 0;
+}
+
+.trade-context-card__arrow {
+ color: var(--gf-primary);
+ flex-shrink: 0;
+}
+
+.trade-context-card__status {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+ font-family: var(--gf-font-body);
+}
+
+.trade-status {
+ font-size: 0.6875rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 0.125rem 0.5rem;
+ border-radius: var(--gf-radius-full);
+}
+
+.trade-status--pending {
+ background: var(--gf-warning-bg);
+ color: #b47d00;
+}
+
+.trade-status--accepted {
+ background: var(--gf-success-bg);
+ color: #1a8a4a;
+}
+
+.trade-status--confirmed {
+ background: var(--gf-info-bg);
+ color: #1a6fb5;
+}
+
+.trade-context-card__expires {
+ font-size: 0.6875rem;
+ color: var(--gf-neutral-400);
+}
+
+.trade-context-card__actions {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+}
+
+.trade-context-card__actions .btn {
+ font-size: 0.75rem;
+ padding: 0.375rem 0.875rem;
+}
+
+/* Button variants for chat */
+.btn--secondary-outline {
+ background: none;
+ border: 1.5px solid var(--gf-secondary);
+ color: var(--gf-secondary);
+ border-radius: var(--gf-radius-full);
+ font-family: var(--gf-font-body);
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.btn--secondary-outline:active {
+ background: var(--gf-secondary-bg);
+}
+
+
+/* ============================================
+ Chat Messages Area
+ ============================================ */
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 0.75rem 0.75rem 1rem;
+ background: var(--gf-bg-subtle);
+ -webkit-overflow-scrolling: touch;
+}
+
+/* Date Separator */
+.chat-date-separator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0.75rem 0;
+}
+
+.chat-date-separator span {
+ font-family: var(--gf-font-body);
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--gf-neutral-400);
+ background: var(--gf-neutral-100);
+ padding: 0.1875rem 0.75rem;
+ border-radius: var(--gf-radius-full);
+}
+
+/* System Messages */
+.chat-system-msg {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.375rem;
+ font-family: var(--gf-font-body);
+ font-size: 0.6875rem;
+ color: var(--gf-neutral-400);
+ text-align: center;
+ margin: 0.5rem 1rem;
+ padding: 0.375rem 0;
+}
+
+.chat-system-msg svg {
+ flex-shrink: 0;
+}
+
+.chat-system-msg--success {
+ color: #1a8a4a;
+}
+
+
+/* ============================================
+ Message Bubbles
+ ============================================ */
+
+.chat-msg {
+ display: flex;
+ align-items: flex-end;
+ gap: 0.375rem;
+ margin-bottom: 0.25rem;
+ max-width: 85%;
+}
+
+/* Incoming */
+.chat-msg--in {
+ align-self: flex-start;
+ margin-right: auto;
+}
+
+.chat-msg--in .chat-msg__bubble {
+ background: var(--gf-bg);
+ color: var(--gf-neutral-800);
+ border-radius: var(--gf-radius-lg) var(--gf-radius-lg) var(--gf-radius-lg) 0.25rem;
+ box-shadow: var(--gf-shadow-sm);
+}
+
+/* Outgoing */
+.chat-msg--out {
+ flex-direction: row-reverse;
+ align-self: flex-end;
+ margin-left: auto;
+}
+
+.chat-msg--out .chat-msg__bubble {
+ background: var(--gf-primary);
+ color: #fff;
+ border-radius: var(--gf-radius-lg) var(--gf-radius-lg) 0.25rem var(--gf-radius-lg);
+}
+
+.chat-msg--out .chat-msg__time {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+/* Avatar */
+.chat-msg__avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: var(--gf-radius-full);
+ object-fit: cover;
+ flex-shrink: 0;
+ background: var(--gf-neutral-200);
+}
+
+/* Bubble */
+.chat-msg__bubble {
+ padding: 0.5rem 0.75rem;
+ max-width: 100%;
+}
+
+.chat-msg__bubble p {
+ font-family: var(--gf-font-body);
+ font-size: 0.875rem;
+ line-height: 1.45;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.chat-msg__time {
+ display: block;
+ font-family: var(--gf-font-body);
+ font-size: 0.625rem;
+ color: var(--gf-neutral-400);
+ margin-top: 0.25rem;
+ text-align: right;
+}
+
+/* Consecutive messages from same sender โ tighten spacing */
+.chat-msg + .chat-msg--in,
+.chat-msg + .chat-msg--out {
+ margin-top: 0.125rem;
+}
+
+/* Typing indicator */
+.chat-msg--typing .chat-msg__bubble {
+ padding: 0.625rem 0.875rem;
+}
+
+.typing-dots {
+ display: flex;
+ gap: 0.25rem;
+ align-items: center;
+ height: 1rem;
+}
+
+.typing-dots span {
+ width: 6px;
+ height: 6px;
+ background: var(--gf-neutral-300);
+ border-radius: var(--gf-radius-full);
+ animation: typingBounce 1.4s infinite ease-in-out;
+}
+
+.typing-dots span:nth-child(2) {
+ animation-delay: 0.2s;
+}
+
+.typing-dots span:nth-child(3) {
+ animation-delay: 0.4s;
+}
+
+@keyframes typingBounce {
+ 0%, 60%, 100% {
+ transform: translateY(0);
+ opacity: 0.4;
+ }
+ 30% {
+ transform: translateY(-4px);
+ opacity: 1;
+ }
+}
+
+
+/* ============================================
+ Chat Input Bar
+ ============================================ */
+
+.chat-input-bar {
+ display: flex;
+ align-items: flex-end;
+ gap: 0.375rem;
+ padding: 0.5rem 0.625rem;
+ background: var(--gf-bg);
+ border-top: 1px solid var(--gf-neutral-100);
+ /* Safe area for notch phones */
+ padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
+}
+
+.chat-input-bar__attach {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ background: none;
+ border: none;
+ color: var(--gf-neutral-500);
+ cursor: pointer;
+ border-radius: var(--gf-radius-full);
+ flex-shrink: 0;
+ transition: color 0.12s ease;
+}
+
+.chat-input-bar__attach:active {
+ color: var(--gf-primary);
+ background: var(--gf-primary-bg);
+}
+
+.chat-input-bar__field {
+ flex: 1;
+ min-width: 0;
+}
+
+.chat-input-bar__textarea {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1.5px solid var(--gf-neutral-200);
+ border-radius: 1.125rem;
+ background: var(--gf-bg-muted);
+ font-family: var(--gf-font-body);
+ font-size: 0.875rem;
+ color: var(--gf-neutral-900);
+ line-height: 1.4;
+ resize: none;
+ max-height: 120px;
+ overflow-y: auto;
+ transition: border-color 0.15s ease;
+}
+
+.chat-input-bar__textarea:focus {
+ outline: none;
+ border-color: var(--gf-primary);
+ background: var(--gf-bg);
+}
+
+.chat-input-bar__textarea::placeholder {
+ color: var(--gf-neutral-400);
+}
+
+.chat-input-bar__send {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ background: var(--gf-primary);
+ border: none;
+ color: #fff;
+ border-radius: var(--gf-radius-full);
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: all 0.15s ease;
+}
+
+.chat-input-bar__send:disabled {
+ background: var(--gf-neutral-200);
+ color: var(--gf-neutral-400);
+ cursor: default;
+}
+
+.chat-input-bar__send:not(:disabled):active {
+ background: var(--gf-primary-dark);
+ transform: scale(0.94);
+}
+
+
+/* ============================================
+ Quick Actions Panel
+ ============================================ */
+
+.chat-quick-actions {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ background: var(--gf-bg);
+ border-top: 1px solid var(--gf-neutral-100);
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.chat-quick-action {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.5rem 0.75rem;
+ background: var(--gf-neutral-50);
+ border: 1.5px solid var(--gf-neutral-200);
+ border-radius: var(--gf-radius-full);
+ font-family: var(--gf-font-body);
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: var(--gf-neutral-700);
+ white-space: nowrap;
+ cursor: pointer;
+ transition: all 0.12s ease;
+}
+
+.chat-quick-action:active {
+ background: var(--gf-primary-bg);
+ border-color: var(--gf-primary-light);
+ color: var(--gf-primary-dark);
+}
+
+
+/* ============================================
+ Bottom Nav Badge (for unread count)
+ ============================================ */
+
+.gf-bottom-nav__badge {
+ position: absolute;
+ top: 2px;
+ right: calc(50% - 20px);
+ min-width: 16px;
+ height: 16px;
+ padding: 0 0.25rem;
+ background: var(--gf-primary);
+ color: #fff;
+ font-family: var(--gf-font-body);
+ font-size: 0.625rem;
+ font-weight: 700;
+ border-radius: var(--gf-radius-full);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+/* ============================================
+ Slide-in Menu / Navigation
+ ============================================ */
+
+.slide-menu-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 999;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.25s ease, visibility 0.25s ease;
+}
+
+.slide-menu-overlay--active {
+ opacity: 1;
+ visibility: visible;
+}
+
+.slide-menu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 80%;
+ max-width: 320px;
+ background: var(--gf-bg);
+ z-index: 1000;
+ transform: translateX(-100%);
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.slide-menu--active {
+ transform: translateX(0);
+}
+
+.slide-menu__header {
+ padding: 1.25rem 1rem;
+ border-bottom: 1px solid var(--gf-neutral-100);
+}
+
+.slide-menu__user {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+}
+
+.slide-menu__user img {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--gf-radius-full);
+ object-fit: cover;
+ background: var(--gf-neutral-200);
+}
+
+.slide-menu__user-name {
+ font-family: var(--gf-font-heading);
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--gf-neutral-900);
+}
+
+.slide-menu__user-handle {
+ font-family: var(--gf-font-body);
+ font-size: 0.75rem;
+ color: var(--gf-neutral-400);
+}
+
+.slide-menu__stats {
+ display: flex;
+ gap: 1rem;
+ font-family: var(--gf-font-body);
+ font-size: 0.75rem;
+ color: var(--gf-neutral-500);
+}
+
+.slide-menu__stats strong {
+ color: var(--gf-neutral-800);
+ font-weight: 700;
+}
+
+.slide-menu__nav {
+ padding: 0.5rem 0;
+ flex: 1;
+}
+
+.slide-menu__link {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ text-decoration: none;
+ font-family: var(--gf-font-body);
+ font-size: 0.9375rem;
+ font-weight: 500;
+ color: var(--gf-neutral-700);
+ transition: background 0.12s ease;
+}
+
+.slide-menu__link:active {
+ background: var(--gf-neutral-50);
+}
+
+.slide-menu__link--active {
+ color: var(--gf-primary);
+ font-weight: 600;
+ background: var(--gf-primary-bg);
+}
+
+.slide-menu__link svg {
+ width: 22px;
+ height: 22px;
+ flex-shrink: 0;
+}
+
+.slide-menu__divider {
+ height: 1px;
+ background: var(--gf-neutral-100);
+ margin: 0.375rem 0;
+}
+
+.slide-menu__footer {
+ padding: 1rem;
+ border-top: 1px solid var(--gf-neutral-100);
+}
+
+.slide-menu__footer-link {
+ font-family: var(--gf-font-body);
+ font-size: 0.8125rem;
+ color: var(--gf-neutral-400);
+ text-decoration: none;
+}
+
+
+/* ============================================
+ Responsive โ Tablet & Desktop
+ ============================================ */
+
+@media (min-width: 768px) {
+ .chat-msg {
+ max-width: 65%;
+ }
+
+ .messages-inbox .container {
+ max-width: 640px;
+ margin: 0 auto;
+ }
+
+ .chat-header {
+ padding: 0.5rem 1.5rem;
+ }
+
+ .chat-messages {
+ padding: 1rem 1.5rem;
+ }
+
+ .chat-input-bar {
+ padding: 0.625rem 1.5rem;
+ }
+
+ .slide-menu {
+ max-width: 360px;
+ }
+
+ .conversation-item__trade-tag {
+ max-width: 280px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .chat-msg {
+ max-width: 50%;
+ }
+
+ .messages-inbox .container {
+ max-width: 720px;
+ }
+}
diff --git a/css/landing.css b/css/landing.css
new file mode 100644
index 0000000..20f268b
--- /dev/null
+++ b/css/landing.css
@@ -0,0 +1,709 @@
+/* ============================================
+ GrubFlip Landing Page
+ Mobile-first ยท Brand tokens from Ava
+ ============================================ */
+
+/* --- Tokens (from design system) --- */
+:root {
+ --gf-primary: #FF6B35;
+ --gf-primary-light: #FF8F66;
+ --gf-primary-dark: #D94E1F;
+ --gf-primary-bg: #FFF3ED;
+
+ --gf-secondary: #2D6A4F;
+ --gf-secondary-light: #40916C;
+ --gf-secondary-dark: #1B4332;
+ --gf-secondary-bg: #EDF5F0;
+
+ --gf-accent: #FFBA08;
+ --gf-accent-light: #FFD166;
+ --gf-accent-bg: #FFF8E1;
+
+ --gf-neutral-50: #FAFAFA;
+ --gf-neutral-100: #F5F5F5;
+ --gf-neutral-200: #E5E5E5;
+ --gf-neutral-400: #A3A3A3;
+ --gf-neutral-500: #737373;
+ --gf-neutral-600: #525252;
+ --gf-neutral-700: #404040;
+ --gf-neutral-800: #262626;
+ --gf-neutral-900: #171717;
+
+ --gf-font-heading: 'Plus Jakarta Sans', sans-serif;
+ --gf-font-body: 'Inter', sans-serif;
+
+ --gf-shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+ --gf-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
+ --gf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
+ --gf-shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
+
+ --gf-radius-sm: 0.375rem;
+ --gf-radius-md: 0.5rem;
+ --gf-radius-lg: 0.75rem;
+ --gf-radius-xl: 1rem;
+ --gf-radius-2xl: 1.5rem;
+ --gf-radius-full: 9999px;
+}
+
+/* --- Reset --- */
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-size: 16px;
+ -webkit-text-size-adjust: 100%;
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: var(--gf-font-body);
+ color: var(--gf-neutral-800);
+ background: #fff;
+ line-height: 1.6;
+ overflow-x: hidden;
+ -webkit-font-smoothing: antialiased;
+}
+
+img { max-width: 100%; display: block; }
+a { text-decoration: none; color: inherit; }
+
+.container {
+ width: 100%;
+ max-width: 1120px;
+ margin: 0 auto;
+ padding: 0 1.25rem;
+}
+
+/* --- Buttons --- */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ font-family: var(--gf-font-body);
+ font-weight: 600;
+ border: none;
+ border-radius: var(--gf-radius-full);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+ line-height: 1;
+ white-space: nowrap;
+}
+
+.btn--sm {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+}
+
+.btn--lg {
+ padding: 0.875rem 1.75rem;
+ font-size: 1rem;
+}
+
+.btn--primary {
+ background: var(--gf-primary);
+ color: #fff;
+}
+.btn--primary:hover {
+ background: var(--gf-primary-dark);
+ transform: translateY(-1px);
+ box-shadow: var(--gf-shadow-md);
+}
+
+.btn--outline {
+ background: transparent;
+ color: var(--gf-neutral-700);
+ border: 2px solid var(--gf-neutral-200);
+}
+.btn--outline:hover {
+ border-color: var(--gf-primary);
+ color: var(--gf-primary);
+}
+
+.btn--dark {
+ background: var(--gf-neutral-900);
+ color: #fff;
+}
+.btn--dark:hover {
+ background: var(--gf-neutral-700);
+ transform: translateY(-1px);
+ box-shadow: var(--gf-shadow-md);
+}
+
+/* --- Navigation --- */
+.nav {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(255,255,255,0.92);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border-bottom: 1px solid rgba(0,0,0,0.06);
+}
+
+.nav__inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: 1120px;
+ margin: 0 auto;
+ padding: 0.875rem 1.25rem;
+}
+
+.nav__logo {
+ font-family: var(--gf-font-heading);
+ font-weight: 800;
+ font-size: 1.375rem;
+ color: var(--gf-neutral-900);
+}
+.nav__logo span {
+ color: var(--gf-primary);
+}
+
+.nav__links {
+ display: flex;
+ align-items: center;
+ gap: 1.25rem;
+}
+
+.nav__link {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--gf-neutral-600);
+ transition: color 0.2s;
+}
+.nav__link:hover { color: var(--gf-primary); }
+
+.hide-mobile { display: none; }
+
+/* --- Hero --- */
+.hero {
+ padding: 7rem 0 3rem;
+ background: linear-gradient(180deg, var(--gf-primary-bg) 0%, #fff 100%);
+}
+
+.hero .container {
+ display: flex;
+ flex-direction: column;
+ gap: 2.5rem;
+}
+
+.hero__badge {
+ display: inline-block;
+ background: #fff;
+ border: 1px solid var(--gf-neutral-200);
+ border-radius: var(--gf-radius-full);
+ padding: 0.375rem 0.875rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--gf-neutral-700);
+ margin-bottom: 0.75rem;
+}
+
+.hero__title {
+ font-family: var(--gf-font-heading);
+ font-weight: 800;
+ font-size: 2.25rem;
+ line-height: 1.15;
+ color: var(--gf-neutral-900);
+ letter-spacing: -0.02em;
+}
+
+.hero__highlight {
+ color: var(--gf-primary);
+}
+
+.hero__sub {
+ margin-top: 1rem;
+ font-size: 1.0625rem;
+ color: var(--gf-neutral-600);
+ line-height: 1.65;
+ max-width: 480px;
+}
+
+.hero__cta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 1.5rem;
+}
+
+.hero__social-proof {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 1.75rem;
+ font-size: 0.875rem;
+ color: var(--gf-neutral-500);
+}
+
+.hero__avatars {
+ display: flex;
+}
+
+.hero__avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ font-size: 0.875rem;
+ border: 2px solid #fff;
+ margin-right: -0.5rem;
+}
+.hero__avatar:last-child { margin-right: 0; }
+
+.hero__social-text strong {
+ color: var(--gf-neutral-800);
+}
+
+/* Phone Mockup */
+.hero__visual {
+ display: flex;
+ justify-content: center;
+}
+
+.phone-mockup {
+ width: 260px;
+ background: var(--gf-neutral-900);
+ border-radius: 2rem;
+ padding: 0.5rem;
+ box-shadow: var(--gf-shadow-xl), 0 0 0 1px rgba(0,0,0,0.1);
+ transform: rotate(2deg);
+}
+
+.phone-mockup__screen {
+ background: #fff;
+ border-radius: 1.625rem;
+ padding: 1rem;
+ min-height: 380px;
+ display: flex;
+ flex-direction: column;
+ gap: 0.625rem;
+}
+
+.phone-mockup__header {
+ text-align: center;
+ padding: 0.5rem 0 0.75rem;
+}
+
+.phone-mockup__logo {
+ font-family: var(--gf-font-heading);
+ font-weight: 800;
+ font-size: 1rem;
+ color: var(--gf-neutral-900);
+}
+.phone-mockup__logo span { color: var(--gf-primary); }
+
+.phone-mockup__card {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ background: var(--gf-neutral-50);
+ border: 1px solid var(--gf-neutral-200);
+ border-radius: var(--gf-radius-lg);
+ padding: 0.75rem;
+ animation: cardSlideIn 0.6s ease both;
+}
+.phone-mockup__card:nth-child(3) { animation-delay: 0.2s; }
+.phone-mockup__card:nth-child(4) { animation-delay: 0.4s; }
+
+.phone-mockup__card--alt {
+ background: var(--gf-secondary-bg);
+ border-color: rgba(45,106,79,0.15);
+}
+
+.phone-mockup__food-emoji {
+ font-size: 1.75rem;
+ flex-shrink: 0;
+}
+
+.phone-mockup__card-text {
+ display: flex;
+ flex-direction: column;
+ gap: 0.125rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.phone-mockup__card-text strong {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--gf-neutral-800);
+}
+
+.phone-mockup__card-text small {
+ font-size: 0.6875rem;
+ color: var(--gf-neutral-400);
+}
+
+.phone-mockup__badge {
+ flex-shrink: 0;
+ background: var(--gf-primary);
+ color: #fff;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ padding: 0.25rem 0.625rem;
+ border-radius: var(--gf-radius-full);
+}
+
+.phone-mockup__badge--green {
+ background: var(--gf-secondary);
+}
+
+@keyframes cardSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* --- How It Works --- */
+.how-it-works {
+ padding: 4rem 0;
+}
+
+.section-title {
+ font-family: var(--gf-font-heading);
+ font-weight: 800;
+ font-size: 1.75rem;
+ color: var(--gf-neutral-900);
+ text-align: center;
+ letter-spacing: -0.02em;
+}
+
+.section-sub {
+ text-align: center;
+ color: var(--gf-neutral-500);
+ margin-top: 0.5rem;
+ font-size: 1rem;
+}
+
+.steps {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ margin-top: 2.5rem;
+}
+
+.step {
+ text-align: center;
+ position: relative;
+ padding: 1.5rem;
+}
+
+.step__icon {
+ font-size: 2.5rem;
+ margin-bottom: 0.75rem;
+}
+
+.step__num {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ background: var(--gf-primary);
+ color: #fff;
+ font-weight: 700;
+ font-size: 0.875rem;
+ margin-bottom: 0.75rem;
+}
+
+.step__title {
+ font-family: var(--gf-font-heading);
+ font-weight: 700;
+ font-size: 1.125rem;
+ color: var(--gf-neutral-800);
+ margin-bottom: 0.5rem;
+}
+
+.step__desc {
+ color: var(--gf-neutral-500);
+ font-size: 0.9375rem;
+ line-height: 1.6;
+ max-width: 320px;
+ margin: 0 auto;
+}
+
+/* --- Features --- */
+.features {
+ padding: 4rem 0;
+ background: var(--gf-neutral-50);
+}
+
+.features__grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1.25rem;
+ margin-top: 2.5rem;
+}
+
+.feature-card {
+ background: #fff;
+ border: 1px solid var(--gf-neutral-200);
+ border-radius: var(--gf-radius-xl);
+ padding: 1.5rem;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+.feature-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--gf-shadow-md);
+}
+
+.feature-card__icon {
+ font-size: 2rem;
+ margin-bottom: 0.75rem;
+}
+
+.feature-card__title {
+ font-family: var(--gf-font-heading);
+ font-weight: 700;
+ font-size: 1.0625rem;
+ color: var(--gf-neutral-800);
+ margin-bottom: 0.375rem;
+}
+
+.feature-card__desc {
+ color: var(--gf-neutral-500);
+ font-size: 0.9375rem;
+ line-height: 1.55;
+}
+
+/* --- Testimonial --- */
+.testimonial {
+ padding: 4rem 0;
+ text-align: center;
+}
+
+.testimonial__quote {
+ font-family: var(--gf-font-heading);
+ font-size: 1.25rem;
+ font-weight: 600;
+ line-height: 1.5;
+ color: var(--gf-neutral-800);
+ max-width: 600px;
+ margin: 0 auto;
+ font-style: italic;
+}
+
+.testimonial__author {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ margin-top: 1.5rem;
+}
+
+.testimonial__avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.75rem;
+ height: 2.75rem;
+ background: var(--gf-accent-bg);
+ border-radius: 50%;
+ font-size: 1.25rem;
+}
+
+.testimonial__author strong {
+ display: block;
+ font-size: 0.9375rem;
+ color: var(--gf-neutral-800);
+}
+
+.testimonial__author span {
+ font-size: 0.8125rem;
+ color: var(--gf-neutral-500);
+}
+
+/* --- Download CTA --- */
+.download {
+ padding: 4rem 0;
+ background: linear-gradient(135deg, var(--gf-primary) 0%, var(--gf-primary-dark) 100%);
+ text-align: center;
+ color: #fff;
+}
+
+.download__title {
+ font-family: var(--gf-font-heading);
+ font-weight: 800;
+ font-size: 2rem;
+ letter-spacing: -0.02em;
+}
+
+.download__sub {
+ margin-top: 0.75rem;
+ font-size: 1.0625rem;
+ opacity: 0.9;
+ max-width: 400px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.download__buttons {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.75rem;
+ margin-top: 2rem;
+}
+
+.download__store-btn {
+ min-width: 160px;
+}
+
+.download__store-icon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+}
+
+.download__note {
+ margin-top: 1.5rem;
+ font-size: 0.875rem;
+ opacity: 0.75;
+}
+.download__note a {
+ color: #fff;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+.download__note a:hover {
+ opacity: 0.8;
+}
+
+/* --- Footer --- */
+.footer {
+ padding: 2.5rem 0 1.5rem;
+ background: var(--gf-neutral-900);
+ color: var(--gf-neutral-400);
+}
+
+.footer__inner {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.footer__logo {
+ font-family: var(--gf-font-heading);
+ font-weight: 800;
+ font-size: 1.25rem;
+ color: #fff;
+}
+.footer__logo span { color: var(--gf-primary); }
+
+.footer__tagline {
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+}
+
+.footer__links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.25rem;
+}
+
+.footer__links a {
+ font-size: 0.875rem;
+ color: var(--gf-neutral-400);
+ transition: color 0.2s;
+}
+.footer__links a:hover { color: #fff; }
+
+.footer__copy {
+ text-align: center;
+ font-size: 0.8125rem;
+ padding-top: 1.5rem;
+ color: var(--gf-neutral-500);
+}
+
+/* ============================================
+ Tablet+ (640px)
+ ============================================ */
+@media (min-width: 640px) {
+ .hide-mobile { display: inline; }
+
+ .hero__title { font-size: 2.75rem; }
+
+ .steps {
+ flex-direction: row;
+ gap: 1.5rem;
+ }
+ .step { flex: 1; }
+
+ .features__grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .footer__inner {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+}
+
+/* ============================================
+ Desktop (960px)
+ ============================================ */
+@media (min-width: 960px) {
+ .hero .container {
+ flex-direction: row;
+ align-items: center;
+ gap: 4rem;
+ }
+
+ .hero__content { flex: 1; }
+ .hero__visual { flex: 0 0 auto; }
+
+ .hero__title { font-size: 3.25rem; }
+
+ .hero {
+ padding: 8rem 0 5rem;
+ }
+
+ .section-title { font-size: 2.25rem; }
+
+ .how-it-works,
+ .features,
+ .testimonial {
+ padding: 5rem 0;
+ }
+
+ .features__grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .testimonial__quote {
+ font-size: 1.5rem;
+ }
+
+ .download {
+ padding: 5rem 0;
+ }
+
+ .download__title { font-size: 2.5rem; }
+
+ .phone-mockup {
+ width: 300px;
+ }
+
+ .phone-mockup__screen {
+ min-height: 440px;
+ }
+}
diff --git a/img/avatar-placeholder.svg b/img/avatar-placeholder.svg
new file mode 100644
index 0000000..4900497
--- /dev/null
+++ b/img/avatar-placeholder.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..8162348
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,237 @@
+/* ============================================
+ GrubFlip โ Core App Module
+ API client, auth, utilities, toast system
+ ============================================ */
+
+const GrubFlip = (() => {
+ 'use strict';
+
+ // --- Config ---
+ const API_BASE = '/api';
+ let authToken = localStorage.getItem('gf_token') || null;
+
+ // --- Auth ---
+ function setToken(token) {
+ authToken = token;
+ if (token) {
+ localStorage.setItem('gf_token', token);
+ } else {
+ localStorage.removeItem('gf_token');
+ }
+ }
+
+ function getToken() {
+ return authToken;
+ }
+
+ function isLoggedIn() {
+ return !!authToken;
+ }
+
+ // --- API Client ---
+ async function api(endpoint, options = {}) {
+ const url = `${API_BASE}${endpoint}`;
+ const headers = {
+ 'Accept': 'application/json',
+ ...options.headers,
+ };
+
+ if (authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+
+ // Don't set Content-Type for FormData (browser sets boundary)
+ if (options.body && !(options.body instanceof FormData)) {
+ headers['Content-Type'] = 'application/json';
+ if (typeof options.body === 'object') {
+ options.body = JSON.stringify(options.body);
+ }
+ }
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ });
+
+ if (response.status === 401) {
+ setToken(null);
+ window.location.href = '/grubflip/login.html';
+ return null;
+ }
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new ApiError(
+ data.error || data.message || `Request failed (${response.status})`,
+ response.status,
+ data
+ );
+ }
+
+ return data;
+ } catch (err) {
+ if (err instanceof ApiError) throw err;
+ throw new ApiError('Network error โ check your connection', 0);
+ }
+ }
+
+ class ApiError extends Error {
+ constructor(message, status, data = null) {
+ super(message);
+ this.name = 'ApiError';
+ this.status = status;
+ this.data = data;
+ }
+ }
+
+ // Convenience methods
+ api.get = (endpoint, params = {}) => {
+ const query = new URLSearchParams(params).toString();
+ const url = query ? `${endpoint}?${query}` : endpoint;
+ return api(url, { method: 'GET' });
+ };
+
+ api.post = (endpoint, body) => {
+ return api(endpoint, { method: 'POST', body });
+ };
+
+ api.put = (endpoint, body) => {
+ return api(endpoint, { method: 'PUT', body });
+ };
+
+ api.delete = (endpoint) => {
+ return api(endpoint, { method: 'DELETE' });
+ };
+
+ api.upload = (endpoint, file, fieldName = 'file') => {
+ const formData = new FormData();
+ formData.append(fieldName, file);
+ return api(endpoint, { method: 'POST', body: formData });
+ };
+
+ // --- Toast System ---
+ let toastContainer = null;
+
+ function ensureToastContainer() {
+ if (!toastContainer) {
+ toastContainer = document.createElement('div');
+ toastContainer.className = 'toast-container';
+ toastContainer.setAttribute('role', 'status');
+ toastContainer.setAttribute('aria-live', 'polite');
+ document.body.appendChild(toastContainer);
+ }
+ return toastContainer;
+ }
+
+ function toast(message, type = 'default', duration = 3000) {
+ const container = ensureToastContainer();
+ const el = document.createElement('div');
+ el.className = `toast${type !== 'default' ? ` toast--${type}` : ''}`;
+ el.textContent = message;
+ container.appendChild(el);
+
+ setTimeout(() => {
+ el.style.opacity = '0';
+ el.style.transform = 'translateY(-0.5rem)';
+ el.style.transition = 'all 0.2s';
+ setTimeout(() => el.remove(), 200);
+ }, duration);
+ }
+
+ // --- Time Helpers ---
+ function timeAgo(dateStr) {
+ const now = Date.now();
+ const then = new Date(dateStr).getTime();
+ const diff = Math.max(0, now - then);
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return 'just now';
+ if (minutes < 60) return `${minutes}m ago`;
+ if (hours < 24) return `${hours}h ago`;
+ if (days < 7) return `${days}d ago`;
+ return new Date(dateStr).toLocaleDateString();
+ }
+
+ function timeUntil(dateStr) {
+ const now = Date.now();
+ const then = new Date(dateStr).getTime();
+ const diff = Math.max(0, then - now);
+ const hours = Math.floor(diff / 3600000);
+ const minutes = Math.floor((diff % 3600000) / 60000);
+
+ if (diff <= 0) return 'expired';
+ if (hours > 0) return `${hours}h ${minutes}m left`;
+ return `${minutes}m left`;
+ }
+
+ // --- DOM Helpers ---
+ function $(selector, parent = document) {
+ return parent.querySelector(selector);
+ }
+
+ function $$(selector, parent = document) {
+ return [...parent.querySelectorAll(selector)];
+ }
+
+ function el(tag, attrs = {}, children = []) {
+ const element = document.createElement(tag);
+ for (const [key, val] of Object.entries(attrs)) {
+ if (key === 'className') element.className = val;
+ else if (key === 'textContent') element.textContent = val;
+ else if (key === 'innerHTML') element.innerHTML = val;
+ else if (key.startsWith('on')) element.addEventListener(key.slice(2).toLowerCase(), val);
+ else if (key === 'dataset') Object.assign(element.dataset, val);
+ else element.setAttribute(key, val);
+ }
+ for (const child of children) {
+ if (typeof child === 'string') {
+ element.appendChild(document.createTextNode(child));
+ } else if (child) {
+ element.appendChild(child);
+ }
+ }
+ return element;
+ }
+
+ // --- Star rating HTML ---
+ function starsHTML(rating, count = null) {
+ const full = Math.floor(rating);
+ const half = rating % 1 >= 0.5 ? 1 : 0;
+ const empty = 5 - full - half;
+ let html = '';
+ for (let i = 0; i < full; i++) html += 'โ
';
+ if (half) html += 'โ
'; // simplified โ half star shown as full
+ for (let i = 0; i < empty; i++) html += 'โ';
+ if (count !== null) html += ` (${count}) `;
+ html += ' ';
+ return html;
+ }
+
+ // --- Debounce ---
+ function debounce(fn, delay = 300) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => fn(...args), delay);
+ };
+ }
+
+ // --- Public API ---
+ return {
+ api,
+ ApiError,
+ setToken,
+ getToken,
+ isLoggedIn,
+ toast,
+ timeAgo,
+ timeUntil,
+ $, $$, el,
+ starsHTML,
+ debounce,
+ };
+})();
diff --git a/js/landing.js b/js/landing.js
new file mode 100644
index 0000000..58f1723
--- /dev/null
+++ b/js/landing.js
@@ -0,0 +1,58 @@
+/* ============================================
+ GrubFlip Landing Page โ JS
+ Smooth scroll, nav background, scroll reveal
+ ============================================ */
+
+(function () {
+ 'use strict';
+
+ // --- Smooth scroll for anchor links ---
+ document.querySelectorAll('a[href^="#"]').forEach(function (link) {
+ link.addEventListener('click', function (e) {
+ var target = document.querySelector(this.getAttribute('href'));
+ if (target) {
+ e.preventDefault();
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ });
+ });
+
+ // --- Nav background on scroll ---
+ var nav = document.querySelector('.nav');
+ if (nav) {
+ var onScroll = function () {
+ if (window.scrollY > 20) {
+ nav.classList.add('nav--scrolled');
+ } else {
+ nav.classList.remove('nav--scrolled');
+ }
+ };
+ window.addEventListener('scroll', onScroll, { passive: true });
+ onScroll();
+ }
+
+ // --- Scroll reveal (IntersectionObserver) ---
+ var revealTargets = document.querySelectorAll(
+ '.step, .feature-card, .testimonial__quote, .download__title'
+ );
+
+ if ('IntersectionObserver' in window && revealTargets.length) {
+ revealTargets.forEach(function (el) {
+ el.style.opacity = '0';
+ el.style.transform = 'translateY(24px)';
+ el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
+ });
+
+ var observer = new IntersectionObserver(function (entries) {
+ entries.forEach(function (entry) {
+ if (entry.isIntersecting) {
+ entry.target.style.opacity = '1';
+ entry.target.style.transform = 'translateY(0)';
+ observer.unobserve(entry.target);
+ }
+ });
+ }, { threshold: 0.15 });
+
+ revealTargets.forEach(function (el) { observer.observe(el); });
+ }
+})();
diff --git a/js/post.js b/js/post.js
new file mode 100644
index 0000000..8a78120
--- /dev/null
+++ b/js/post.js
@@ -0,0 +1,249 @@
+/* ============================================
+ GrubFlip โ Post Meal Form
+ Image upload, dietary tag picker, meal
+ creation with preview.
+ ============================================ */
+
+const PostMeal = (() => {
+ 'use strict';
+
+ const { api, $, $$, el, toast } = GrubFlip;
+
+ let uploadedImage = null; // { url, thumb_url, upload_id }
+ let submitting = false;
+
+ const DIETARY_TAGS = [
+ 'vegan', 'vegetarian', 'gluten-free', 'dairy', 'dairy-free',
+ 'nut-free', 'halal', 'kosher', 'high-protein', 'low-carb', 'spicy'
+ ];
+
+ const CUISINES = [
+ 'Italian', 'Mexican', 'Chinese', 'Japanese', 'Indian', 'Thai',
+ 'Mediterranean', 'American', 'Korean', 'Vietnamese', 'French',
+ 'Middle Eastern', 'Caribbean', 'Ethiopian', 'Other'
+ ];
+
+ // --- Image Upload ---
+ function setupImageUpload() {
+ const zone = $('#upload-zone');
+ const fileInput = $('#image-input');
+ if (!zone || !fileInput) return;
+
+ // Click to select
+ zone.addEventListener('click', (e) => {
+ if (e.target.closest('.btn--remove')) return;
+ fileInput.click();
+ });
+
+ // File selected
+ fileInput.addEventListener('change', () => {
+ if (fileInput.files.length) handleFile(fileInput.files[0]);
+ });
+
+ // Drag and drop
+ zone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ zone.classList.add('dragover');
+ });
+
+ zone.addEventListener('dragleave', () => {
+ zone.classList.remove('dragover');
+ });
+
+ zone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ zone.classList.remove('dragover');
+ const file = e.dataTransfer.files[0];
+ if (file) handleFile(file);
+ });
+
+ // Remove button
+ const removeBtn = zone.querySelector('.btn--remove');
+ if (removeBtn) {
+ removeBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ clearUpload();
+ });
+ }
+ }
+
+ async function handleFile(file) {
+ // Validate
+ const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
+ if (!validTypes.includes(file.type)) {
+ toast('Please upload a JPG, PNG, or WebP image', 'error');
+ return;
+ }
+
+ if (file.size > 20 * 1024 * 1024) {
+ toast('Image must be under 20MB', 'error');
+ return;
+ }
+
+ const zone = $('#upload-zone');
+ const previewImg = $('#upload-preview-img');
+
+ // Show local preview immediately
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ previewImg.src = e.target.result;
+ zone.classList.add('has-preview');
+ };
+ reader.readAsDataURL(file);
+
+ // Upload to server
+ try {
+ zone.style.opacity = '0.7';
+ const data = await api.upload('/uploads/image', file);
+ uploadedImage = {
+ url: data.url,
+ thumb_url: data.thumb_url,
+ upload_id: data.upload_id,
+ };
+ toast('Image uploaded!', 'success');
+ } catch (err) {
+ toast('Upload failed โ ' + err.message, 'error');
+ clearUpload();
+ } finally {
+ zone.style.opacity = '';
+ }
+ }
+
+ function clearUpload() {
+ uploadedImage = null;
+ const zone = $('#upload-zone');
+ const fileInput = $('#image-input');
+ const previewImg = $('#upload-preview-img');
+
+ if (zone) zone.classList.remove('has-preview');
+ if (fileInput) fileInput.value = '';
+ if (previewImg) previewImg.src = '';
+ }
+
+ // --- Expiry Picker ---
+ function setupExpiryPicker() {
+ const select = $('#expires-select');
+ if (!select) return;
+
+ // Populate with reasonable time windows
+ const options = [
+ { value: '2', label: '2 hours' },
+ { value: '4', label: '4 hours' },
+ { value: '6', label: '6 hours' },
+ { value: '8', label: '8 hours' },
+ { value: '12', label: '12 hours' },
+ { value: '24', label: '24 hours' },
+ ];
+
+ select.innerHTML = 'Pick up within... ';
+ for (const opt of options) {
+ select.appendChild(el('option', { value: opt.value, textContent: opt.label }));
+ }
+ }
+
+ // --- Form Validation ---
+ function validate() {
+ const errors = [];
+ const title = $('#meal-title')?.value.trim();
+ const description = $('#meal-description')?.value.trim();
+ const expiresHours = $('#expires-select')?.value;
+
+ if (!title || title.length < 3) errors.push({ field: 'meal-title', msg: 'Title must be at least 3 characters' });
+ if (title && title.length > 100) errors.push({ field: 'meal-title', msg: 'Title is too long (max 100 chars)' });
+ if (!description) errors.push({ field: 'meal-description', msg: 'Add a short description' });
+ if (!expiresHours) errors.push({ field: 'expires-select', msg: 'Set a pickup window' });
+ if (!uploadedImage) errors.push({ field: 'upload-zone', msg: 'Add a photo of your meal' });
+
+ // Clear previous errors
+ $$('.form-error').forEach(el => el.remove());
+ $$('.form-input, .form-textarea, .form-select').forEach(el => el.style.borderColor = '');
+
+ // Show errors
+ for (const error of errors) {
+ const fieldEl = $(`#${error.field}`);
+ if (fieldEl) {
+ fieldEl.style.borderColor = 'var(--gf-error)';
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = error.msg;
+ fieldEl.parentElement.appendChild(errEl);
+ }
+ }
+
+ return errors.length === 0;
+ }
+
+ // --- Submit ---
+ async function handleSubmit(e) {
+ e.preventDefault();
+ if (submitting) return;
+ if (!validate()) return;
+
+ submitting = true;
+ const submitBtn = $('#submit-btn');
+ if (submitBtn) {
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Posting...';
+ }
+
+ // Gather selected dietary tags
+ const selectedTags = $$('.tag-picker__option:checked').map(cb => cb.value);
+
+ const expiresHours = parseInt($('#expires-select').value, 10);
+ const expiresAt = new Date(Date.now() + expiresHours * 3600000).toISOString();
+
+ const payload = {
+ title: $('#meal-title').value.trim(),
+ description: $('#meal-description').value.trim(),
+ upload_id: uploadedImage.upload_id,
+ dietary_tags: selectedTags,
+ cuisine: $('#cuisine-select').value || null,
+ portion_size: $('#portion-select').value || 'single',
+ trade_type: $('#trade-type-select')?.value || 'swap',
+ expires_at: expiresAt,
+ };
+
+ try {
+ const result = await api.post('/meals', payload);
+ toast('Meal posted! ๐', 'success');
+ // Redirect to feed after short delay
+ setTimeout(() => {
+ window.location.href = '/grubflip/index.html';
+ }, 1000);
+ } catch (err) {
+ toast('Failed to post โ ' + err.message, 'error');
+ } finally {
+ submitting = false;
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Post Meal';
+ }
+ }
+ }
+
+ // --- Init ---
+ function init() {
+ const form = $('#post-meal-form');
+ if (!form) return;
+
+ setupImageUpload();
+ setupExpiryPicker();
+
+ form.addEventListener('submit', handleSubmit);
+
+ // Character counter for description
+ const desc = $('#meal-description');
+ const counter = $('#desc-counter');
+ if (desc && counter) {
+ desc.addEventListener('input', () => {
+ const len = desc.value.length;
+ counter.textContent = `${len}/300`;
+ counter.style.color = len > 300 ? 'var(--gf-error)' : 'var(--gf-neutral-400)';
+ });
+ }
+ }
+
+ return { init };
+})();
+
+document.addEventListener('DOMContentLoaded', PostMeal.init);
diff --git a/js/trades.js b/js/trades.js
new file mode 100644
index 0000000..ec40ad0
--- /dev/null
+++ b/js/trades.js
@@ -0,0 +1,306 @@
+/* ============================================
+ GrubFlip โ Trade Flow
+ Trade state machine, trade cards, actions
+ ============================================ */
+
+const Trades = (() => {
+ 'use strict';
+
+ const { api, $, $$, el, timeAgo, toast } = GrubFlip;
+
+ // --- State Machine Definition ---
+ const TRANSITIONS = {
+ pending: ['accepted', 'declined', 'cancelled', 'expired'],
+ accepted: ['confirmed', 'declined', 'cancelled'],
+ confirmed: ['completed', 'cancelled'],
+ completed: [], // terminal
+ declined: [], // terminal
+ cancelled: [], // terminal
+ expired: [], // terminal
+ };
+
+ const STATUS_META = {
+ pending: { label: 'Pending', icon: 'โณ', actionable: true },
+ accepted: { label: 'Accepted', icon: '๐', actionable: true },
+ confirmed: { label: 'Confirmed', icon: '๐', actionable: true },
+ completed: { label: 'Completed', icon: 'โ
', actionable: false },
+ declined: { label: 'Declined', icon: 'โ', actionable: false },
+ cancelled: { label: 'Cancelled', icon: '๐ซ', actionable: false },
+ expired: { label: 'Expired', icon: 'โฐ', actionable: false },
+ };
+
+ let trades = [];
+ let activeTab = 'active'; // 'active' | 'completed'
+ let loading = false;
+
+ // --- Trade Card Rendering ---
+ function renderTradeCard(trade, currentUserId) {
+ const card = el('div', { className: 'trade-card', dataset: { tradeId: trade.id } });
+
+ // Status bar
+ const statusBar = el('div', { className: 'trade-card__status-bar' });
+ const meta = STATUS_META[trade.status] || STATUS_META.pending;
+ statusBar.appendChild(el('span', {
+ className: `trade-card__status trade-card__status--${trade.status}`,
+ textContent: `${meta.icon} ${meta.label}`,
+ }));
+ statusBar.appendChild(el('span', {
+ className: 'trade-card__time',
+ textContent: timeAgo(trade.updated_at || trade.created_at),
+ }));
+ card.appendChild(statusBar);
+
+ // Meals being traded
+ const mealsSection = el('div', { className: 'trade-card__meals' });
+
+ // Offered meal (what the requester is offering)
+ const offeredMeal = el('div', { className: 'trade-card__meal' });
+ offeredMeal.appendChild(el('img', {
+ className: 'trade-card__meal-img',
+ src: trade.offered_meal.image_thumb_url,
+ alt: trade.offered_meal.title,
+ loading: 'lazy',
+ }));
+ const offeredInfo = el('div', { className: 'trade-card__meal-info' });
+ offeredInfo.appendChild(el('p', { className: 'trade-card__meal-title', textContent: trade.offered_meal.title }));
+ offeredInfo.appendChild(el('p', { className: 'trade-card__meal-user', textContent: trade.requester.display_name }));
+ offeredMeal.appendChild(offeredInfo);
+ mealsSection.appendChild(offeredMeal);
+
+ // Swap icon
+ mealsSection.appendChild(el('span', {
+ className: 'trade-card__swap-icon',
+ innerHTML: ` `,
+ }));
+
+ // Requested meal (what the requester wants)
+ const requestedMeal = el('div', { className: 'trade-card__meal' });
+ requestedMeal.appendChild(el('img', {
+ className: 'trade-card__meal-img',
+ src: trade.requested_meal.image_thumb_url,
+ alt: trade.requested_meal.title,
+ loading: 'lazy',
+ }));
+ const requestedInfo = el('div', { className: 'trade-card__meal-info' });
+ requestedInfo.appendChild(el('p', { className: 'trade-card__meal-title', textContent: trade.requested_meal.title }));
+ requestedInfo.appendChild(el('p', { className: 'trade-card__meal-user', textContent: trade.owner.display_name }));
+ requestedMeal.appendChild(requestedInfo);
+ mealsSection.appendChild(requestedMeal);
+
+ card.appendChild(mealsSection);
+
+ // Message
+ if (trade.message) {
+ card.appendChild(el('p', {
+ className: 'trade-card__message',
+ textContent: `"${trade.message}"`,
+ }));
+ }
+
+ // Meetup info (if confirmed)
+ if (trade.status === 'confirmed' && (trade.meetup_location || trade.meetup_time)) {
+ const meetup = el('div', {
+ className: 'trade-card__message',
+ style: 'font-style:normal; background:var(--gf-secondary-bg);',
+ });
+ let meetupText = '๐ Meetup: ';
+ if (trade.meetup_location) meetupText += trade.meetup_location;
+ if (trade.meetup_time) meetupText += ` at ${new Date(trade.meetup_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
+ meetup.textContent = meetupText;
+ card.appendChild(meetup);
+ }
+
+ // Action buttons โ based on status and role
+ const actions = getActions(trade, currentUserId);
+ if (actions.length > 0) {
+ const actionsWrap = el('div', { className: 'trade-card__actions' });
+ for (const action of actions) {
+ actionsWrap.appendChild(el('button', {
+ className: `btn btn--${action.style} btn--sm`,
+ textContent: action.label,
+ onClick: () => handleAction(trade.id, action.action),
+ }));
+ }
+ card.appendChild(actionsWrap);
+ }
+
+ return card;
+ }
+
+ // --- Determine Available Actions ---
+ function getActions(trade, currentUserId) {
+ const isOwner = trade.owner.id === currentUserId;
+ const isRequester = trade.requester.id === currentUserId;
+ const actions = [];
+
+ switch (trade.status) {
+ case 'pending':
+ if (isOwner) {
+ actions.push({ label: 'Accept', action: 'accept', style: 'primary' });
+ actions.push({ label: 'Decline', action: 'decline', style: 'ghost' });
+ }
+ if (isRequester) {
+ actions.push({ label: 'Cancel Request', action: 'cancel', style: 'ghost' });
+ }
+ break;
+
+ case 'accepted':
+ actions.push({ label: 'Set Meetup', action: 'set-meetup', style: 'primary' });
+ actions.push({ label: 'Cancel', action: 'cancel', style: 'ghost' });
+ break;
+
+ case 'confirmed':
+ actions.push({ label: 'Mark Complete', action: 'complete', style: 'secondary' });
+ actions.push({ label: 'Cancel', action: 'cancel', style: 'ghost' });
+ break;
+ }
+
+ return actions;
+ }
+
+ // --- Handle Trade Actions ---
+ async function handleAction(tradeId, action) {
+ try {
+ switch (action) {
+ case 'accept':
+ await api.put(`/trades/${tradeId}`, { status: 'accepted' });
+ toast('Trade accepted! Set up a meetup time.', 'success');
+ break;
+
+ case 'decline':
+ if (!confirm('Decline this trade request?')) return;
+ await api.put(`/trades/${tradeId}`, { status: 'declined' });
+ toast('Trade declined.', 'default');
+ break;
+
+ case 'cancel':
+ if (!confirm('Cancel this trade?')) return;
+ await api.put(`/trades/${tradeId}`, { status: 'cancelled' });
+ toast('Trade cancelled.', 'default');
+ break;
+
+ case 'complete':
+ await api.put(`/trades/${tradeId}`, { status: 'completed' });
+ toast('Trade completed! ๐', 'success');
+ break;
+
+ case 'set-meetup':
+ openMeetupModal(tradeId);
+ return; // Don't reload โ modal handles it
+
+ default:
+ return;
+ }
+
+ loadTrades(); // Refresh
+ } catch (err) {
+ toast('Action failed: ' + err.message, 'error');
+ }
+ }
+
+ // --- Meetup Modal ---
+ function openMeetupModal(tradeId) {
+ const overlay = $('#meetup-modal');
+ if (!overlay) return;
+
+ overlay.classList.add('open');
+
+ const form = $('#meetup-form');
+ form.onsubmit = async (e) => {
+ e.preventDefault();
+ const location = $('#meetup-location').value.trim();
+ const time = $('#meetup-time').value;
+
+ if (!location) {
+ toast('Please enter a meetup location', 'warning');
+ return;
+ }
+
+ try {
+ await api.put(`/trades/${tradeId}`, {
+ status: 'confirmed',
+ meetup_location: location,
+ meetup_time: time || null,
+ });
+ toast('Meetup confirmed! ๐', 'success');
+ overlay.classList.remove('open');
+ loadTrades();
+ } catch (err) {
+ toast('Failed to confirm meetup: ' + err.message, 'error');
+ }
+ };
+
+ // Close on overlay click
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) overlay.classList.remove('open');
+ });
+ }
+
+ // --- Load Trades ---
+ async function loadTrades() {
+ if (loading) return;
+ loading = true;
+
+ const list = $('#trades-list');
+ if (!list) return;
+
+ list.innerHTML = '
';
+
+ try {
+ const data = await api.get('/trades', { status: activeTab === 'active' ? 'active' : 'completed' });
+ trades = data.trades || [];
+
+ list.innerHTML = '';
+
+ // TODO: get actual current user ID from auth
+ const currentUserId = parseInt(localStorage.getItem('gf_user_id') || '0', 10);
+
+ if (trades.length === 0) {
+ list.innerHTML = `
+
+
+
+
+
No ${activeTab} trades
+
${activeTab === 'active' ? 'Browse the feed and request a swap!' : 'Your completed trades will show here.'}
+
`;
+ return;
+ }
+
+ for (const trade of trades) {
+ list.appendChild(renderTradeCard(trade, currentUserId));
+ }
+ } catch (err) {
+ toast('Failed to load trades: ' + err.message, 'error');
+ list.innerHTML = '';
+ } finally {
+ loading = false;
+ }
+ }
+
+ // --- Tab Switching ---
+ function setupTabs() {
+ const tabs = $$('.trade-tab');
+ for (const tab of tabs) {
+ tab.addEventListener('click', () => {
+ tabs.forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ activeTab = tab.dataset.tab;
+ loadTrades();
+ });
+ }
+ }
+
+ // --- Init ---
+ function init() {
+ const tradesContainer = $('#trades-container');
+ if (!tradesContainer) return;
+
+ setupTabs();
+ loadTrades();
+ }
+
+ return { init, renderTradeCard, loadTrades };
+})();
+
+document.addEventListener('DOMContentLoaded', Trades.init);
diff --git a/landing.html b/landing.html
new file mode 100644
index 0000000..8e65379
--- /dev/null
+++ b/landing.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+ GrubFlip โ Swap Meals with Restaurant Workers Near You
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
๐ Built for restaurant crews
+
Tired of the same staff meal?Flip it.
+
Trade meals with other restaurant workers nearby. Your pad thai for their pizza slice. Free, fast, and way better than eating the same thing every shift.
+
+
+
+ ๐งโ๐ณ
+ ๐ฉโ๐ณ
+ ๐จโ๐ณ
+
+ 500+ restaurant workers already flipping
+
+
+
+
+
+
+
+
+
๐
+
+ Margherita Slice
+ Marco's Pizzeria ยท 0.2 mi
+
+
Swap
+
+
+
๐
+
+ Pad Thai Bowl
+ Thai Garden ยท 0.4 mi
+
+
Swap
+
+
+
๐ฎ
+
+ Carnitas Tacos (2x)
+ La Cocina ยท 0.3 mi
+
+
Swap
+
+
+
+
+
+
+
+
+
+
+
+
How It Works
+
Three steps. That's it. No accounts to verify, no fees, no BS.
+
+
+
๐ธ
+
1
+
Post Your Meal
+
Snap a pic of your staff meal and post it. Add what restaurant you work at and when you're free to swap.
+
+
+
๐
+
2
+
Find a Flip
+
Browse meals from nearby restaurant workers. See something you want? Send a flip request. They accept, you're in.
+
+
+
๐ค
+
3
+
Meet & Eat
+
Meet up between shifts, swap meals, and enjoy something different. Rate the flip and build your rep.
+
+
+
+
+
+
+
+
+
Why Restaurant Workers Love GrubFlip
+
+
+
๐ธ
+
100% Free
+
No money changes hands. Just meal for meal. The way it should be.
+
+
+
๐
+
Hyper-Local
+
Only see meals within walking distance. Built for your block, not your borough.
+
+
+
โก
+
Shift-Friendly
+
Set your availability around your shifts. GrubFlip works on your schedule, not the other way around.
+
+
+
โญ
+
Rep System
+
Build your reputation as a reliable flipper. The more you trade, the more people trust you.
+
+
+
+
+
+
+
+
+
+ "I've been eating the same chicken parm for 3 years. First week on GrubFlip I had sushi, ramen, and the best empanadas of my life. Game changer."
+
+
+
๐จโ๐ณ
+
+ Danny R.
+ Line cook, Brooklyn
+
+
+
+
+
+
+
+
+
Ready to flip?
+
Join 500+ restaurant workers already trading meals. Available on iOS and Android.
+
+
Or use the web app โ no download needed.
+
+
+
+
+
+
+
+
+
diff --git a/messages.html b/messages.html
new file mode 100644
index 0000000..184ee45
--- /dev/null
+++ b/messages.html
@@ -0,0 +1,185 @@
+
+
+
+
+
+ GrubFlip โ Messages
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
3 active trade chats
+
+
+
+
+
+
+
+
+
+
+
+
+ Feed
+
+
+
+ Post
+
+
+
+ Chat
+ 3
+
+
+
+ Trades
+
+
+
+ Profile
+
+
+
+
+
diff --git a/post.html b/post.html
new file mode 100644
index 0000000..4b857ad
--- /dev/null
+++ b/post.html
@@ -0,0 +1,183 @@
+
+
+
+
+
+ Post a Meal โ GrubFlip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Feed
+
+
+
+ Post
+
+
+
+ Trades
+
+
+
+ Profile
+
+
+
+
+
+
+
diff --git a/trades.html b/trades.html
new file mode 100644
index 0000000..7784035
--- /dev/null
+++ b/trades.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+ My Trades โ GrubFlip
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Active
+ History
+
+
+
+
+
+
+
+
+
+
+
+
Set Meetup Details
+
+
+ Where to meet?
+
+
+
+ When? (optional)
+
+
+ Confirm Meetup
+
+
+
+
+
+
+
+
+ Feed
+
+
+
+ Post
+
+
+
+ Trades
+
+
+
+ Profile
+
+
+
+
+
+
+