Add GrubFlip consumer app — feed, trades, chat, post, landing, admin login
Full vanilla HTML/CSS/JS consumer app with mobile-first responsive design. Pages: feed (index), post meal, trades, messages inbox, chat, landing page, and admin login skeleton. Uses Ava's design tokens throughout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
358f35c6d0
commit
05dd55e0b6
15 changed files with 4652 additions and 0 deletions
974
admin/css/admin.css
Normal file
974
admin/css/admin.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
122
admin/css/tokens.css
Normal file
122
admin/css/tokens.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
194
admin/index.html
Normal file
194
admin/index.html
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Grubflip Admin — Login</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/tokens.css">
|
||||||
|
<link rel="stylesheet" href="css/admin.css">
|
||||||
|
<style>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--gf-bg-dark) 0%, #2a2a4a 100%);
|
||||||
|
padding: var(--gf-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: var(--gf-bg);
|
||||||
|
border-radius: var(--gf-radius-xl);
|
||||||
|
box-shadow: var(--gf-shadow-xl);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 26rem;
|
||||||
|
padding: var(--gf-space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--gf-space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand .logo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
background: var(--gf-primary);
|
||||||
|
border-radius: var(--gf-radius-lg);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: var(--gf-weight-bold);
|
||||||
|
margin-bottom: var(--gf-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand h1 {
|
||||||
|
font-family: var(--gf-font-heading);
|
||||||
|
font-size: var(--gf-text-2xl);
|
||||||
|
color: var(--gf-neutral-900);
|
||||||
|
margin-bottom: var(--gf-space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand p {
|
||||||
|
color: var(--gf-neutral-500);
|
||||||
|
font-size: var(--gf-text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: var(--gf-space-6);
|
||||||
|
font-size: var(--gf-text-sm);
|
||||||
|
color: var(--gf-neutral-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer a {
|
||||||
|
color: var(--gf-primary);
|
||||||
|
font-weight: var(--gf-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error {
|
||||||
|
background: var(--gf-error-bg);
|
||||||
|
color: var(--gf-error);
|
||||||
|
padding: var(--gf-space-3) var(--gf-space-4);
|
||||||
|
border-radius: var(--gf-radius-md);
|
||||||
|
font-size: var(--gf-text-sm);
|
||||||
|
margin-bottom: var(--gf-space-4);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error.visible { display: block; }
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
font-size: var(--gf-text-base);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-brand">
|
||||||
|
<div class="logo">🍔</div>
|
||||||
|
<h1>Grubflip Admin</h1>
|
||||||
|
<p>Restaurant management dashboard</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loginError" class="login-error" role="alert"></div>
|
||||||
|
|
||||||
|
<form id="loginForm" novalidate>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="form-label">Email address</label>
|
||||||
|
<input type="email" id="email" class="form-input" placeholder="owner@restaurant.com" autocomplete="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" id="password" class="form-input" placeholder="Enter your password" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group flex items-center justify-between">
|
||||||
|
<label class="flex items-center gap-2 text-sm" style="cursor:pointer">
|
||||||
|
<input type="checkbox" id="remember" style="accent-color:var(--gf-primary)">
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
<a href="#" class="text-sm">Forgot password?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-login" id="loginBtn">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
Don't have an account? <a href="#">Contact Grubflip</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const loginError = document.getElementById('loginError');
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
loginError.classList.remove('visible');
|
||||||
|
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
loginError.textContent = 'Please enter your email and password.';
|
||||||
|
loginError.classList.add('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginBtn.disabled = true;
|
||||||
|
loginBtn.textContent = 'Signing in…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wire to Mike's API when ready
|
||||||
|
const resp = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(data.message || 'Invalid email or password.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
localStorage.setItem('gf_token', data.token);
|
||||||
|
localStorage.setItem('gf_user', JSON.stringify(data.user));
|
||||||
|
window.location.href = 'dashboard.html';
|
||||||
|
} catch (err) {
|
||||||
|
// For now, allow demo login
|
||||||
|
if (email === 'demo@grubflip.com' && password === 'demo') {
|
||||||
|
localStorage.setItem('gf_token', 'demo-token');
|
||||||
|
localStorage.setItem('gf_user', JSON.stringify({
|
||||||
|
id: 1,
|
||||||
|
name: 'Demo Restaurant',
|
||||||
|
email: 'demo@grubflip.com',
|
||||||
|
role: 'owner'
|
||||||
|
}));
|
||||||
|
window.location.href = 'dashboard.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loginError.textContent = err.message;
|
||||||
|
loginError.classList.add('visible');
|
||||||
|
} finally {
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.textContent = 'Sign In';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
173
chat.html
Normal file
173
chat.html
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>GrubFlip — Chat</title>
|
||||||
|
<meta name="theme-color" content="#FF6B35">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
<link rel="stylesheet" href="css/chat.css">
|
||||||
|
</head>
|
||||||
|
<body class="chat-view">
|
||||||
|
|
||||||
|
<!-- Chat Header -->
|
||||||
|
<header class="chat-header">
|
||||||
|
<a href="messages.html" class="chat-header__back" aria-label="Back to messages">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||||
|
</a>
|
||||||
|
<div class="chat-header__user">
|
||||||
|
<div class="chat-header__avatar">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" width="36" height="36">
|
||||||
|
<span class="chat-header__online" aria-label="Online"></span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-header__info">
|
||||||
|
<h1 class="chat-header__name">Marcus J.</h1>
|
||||||
|
<span class="chat-header__status">Online · ⭐ 4.8 · 23 trades</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" aria-label="More options">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Trade Context Card (pinned at top) -->
|
||||||
|
<div class="trade-context-card">
|
||||||
|
<div class="trade-context-card__meals">
|
||||||
|
<div class="trade-context-card__meal">
|
||||||
|
<span class="trade-context-card__emoji">🍕</span>
|
||||||
|
<span class="trade-context-card__label">Margherita Pizza</span>
|
||||||
|
<span class="trade-context-card__owner">You</span>
|
||||||
|
</div>
|
||||||
|
<div class="trade-context-card__arrow">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="trade-context-card__meal">
|
||||||
|
<span class="trade-context-card__emoji">🍜</span>
|
||||||
|
<span class="trade-context-card__label">Pad Thai</span>
|
||||||
|
<span class="trade-context-card__owner">Marcus</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trade-context-card__status">
|
||||||
|
<span class="trade-status trade-status--accepted">Accepted</span>
|
||||||
|
<span class="trade-context-card__expires">Expires in 2h 15m</span>
|
||||||
|
</div>
|
||||||
|
<div class="trade-context-card__actions">
|
||||||
|
<button class="btn btn--sm btn--secondary-outline">Set Meetup</button>
|
||||||
|
<button class="btn btn--sm btn--primary">Confirm Trade</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages Area -->
|
||||||
|
<main class="chat-messages" id="chatMessages" role="log" aria-label="Chat messages" aria-live="polite">
|
||||||
|
|
||||||
|
<!-- Date Separator -->
|
||||||
|
<div class="chat-date-separator" role="separator">
|
||||||
|
<span>Today</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System message -->
|
||||||
|
<div class="chat-system-msg">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||||
|
Trade request sent for Margherita Pizza ↔ Pad Thai
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incoming message -->
|
||||||
|
<div class="chat-msg chat-msg--in">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" class="chat-msg__avatar" width="32" height="32">
|
||||||
|
<div class="chat-msg__bubble">
|
||||||
|
<p>Hey! Your margherita looks amazing 🤤 I just made a fresh batch of pad thai — extra peanuts and lime. Interested in a swap?</p>
|
||||||
|
<time class="chat-msg__time">4:32 PM</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Outgoing message -->
|
||||||
|
<div class="chat-msg chat-msg--out">
|
||||||
|
<div class="chat-msg__bubble">
|
||||||
|
<p>Absolutely! I just pulled it out of the wood-fired oven. Still warm 🔥</p>
|
||||||
|
<time class="chat-msg__time">4:34 PM</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System message -->
|
||||||
|
<div class="chat-system-msg chat-system-msg--success">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||||
|
Marcus accepted the trade!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incoming message -->
|
||||||
|
<div class="chat-msg chat-msg--in">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" class="chat-msg__avatar" width="32" height="32">
|
||||||
|
<div class="chat-msg__bubble">
|
||||||
|
<p>Awesome! Where do you want to meet? I'm near the downtown area</p>
|
||||||
|
<time class="chat-msg__time">4:36 PM</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Outgoing message -->
|
||||||
|
<div class="chat-msg chat-msg--out">
|
||||||
|
<div class="chat-msg__bubble">
|
||||||
|
<p>I'm at Rosario's on 5th. Can you swing by around 6?</p>
|
||||||
|
<time class="chat-msg__time">4:37 PM</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incoming message with location -->
|
||||||
|
<div class="chat-msg chat-msg--in">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" class="chat-msg__avatar" width="32" height="32">
|
||||||
|
<div class="chat-msg__bubble">
|
||||||
|
<p>Sounds good! I can meet at the corner of 5th and Main around 6pm 👍</p>
|
||||||
|
<time class="chat-msg__time">4:38 PM</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Typing indicator -->
|
||||||
|
<div class="chat-msg chat-msg--in chat-msg--typing" aria-label="Marcus is typing">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" class="chat-msg__avatar" width="32" height="32">
|
||||||
|
<div class="chat-msg__bubble">
|
||||||
|
<div class="typing-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Chat Input -->
|
||||||
|
<footer class="chat-input-bar">
|
||||||
|
<button class="chat-input-bar__attach" aria-label="Attach photo">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="chat-input-bar__field">
|
||||||
|
<textarea
|
||||||
|
class="chat-input-bar__textarea"
|
||||||
|
placeholder="Type a message..."
|
||||||
|
aria-label="Message input"
|
||||||
|
rows="1"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="chat-input-bar__send" aria-label="Send message" disabled>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Quick Actions (slide up from input) -->
|
||||||
|
<div class="chat-quick-actions" id="quickActions" hidden>
|
||||||
|
<button class="chat-quick-action">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
|
Share Location
|
||||||
|
</button>
|
||||||
|
<button class="chat-quick-action">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||||
|
Suggest Meetup Time
|
||||||
|
</button>
|
||||||
|
<button class="chat-quick-action">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||||
|
Complete Trade
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
974
css/chat.css
Normal file
974
css/chat.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
709
css/landing.css
Normal file
709
css/landing.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
img/avatar-placeholder.svg
Normal file
5
img/avatar-placeholder.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||||
|
<circle cx="24" cy="24" r="24" fill="#E5E5E5"/>
|
||||||
|
<circle cx="24" cy="18" r="8" fill="#A3A3A3"/>
|
||||||
|
<path d="M8 42c0-8.837 7.163-16 16-16s16 7.163 16 16" fill="#A3A3A3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
237
js/app.js
Normal file
237
js/app.js
Normal file
|
|
@ -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 = '<span class="stars" aria-label="' + rating + ' out of 5 stars">';
|
||||||
|
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 += ` <span style="color:var(--gf-neutral-400)">(${count})</span>`;
|
||||||
|
html += '</span>';
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
})();
|
||||||
58
js/landing.js
Normal file
58
js/landing.js
Normal file
|
|
@ -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); });
|
||||||
|
}
|
||||||
|
})();
|
||||||
249
js/post.js
Normal file
249
js/post.js
Normal file
|
|
@ -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 = '<option value="">Pick up within...</option>';
|
||||||
|
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);
|
||||||
306
js/trades.js
Normal file
306
js/trades.js
Normal file
|
|
@ -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: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="28" height="28"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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 = '<div class="spinner"></div>';
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="feed__empty">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:64px;height:64px;margin:0 auto 1rem;opacity:0.4">
|
||||||
|
<path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/>
|
||||||
|
</svg>
|
||||||
|
<h3>No ${activeTab} trades</h3>
|
||||||
|
<p>${activeTab === 'active' ? 'Browse the feed and request a swap!' : 'Your completed trades will show here.'}</p>
|
||||||
|
</div>`;
|
||||||
|
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);
|
||||||
202
landing.html
Normal file
202
landing.html
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GrubFlip — Swap Meals with Restaurant Workers Near You</title>
|
||||||
|
<meta name="description" content="GrubFlip is the meal trading app for restaurant workers. Swap staff meals, discover new flavors, save money. Join the flip.">
|
||||||
|
<meta name="theme-color" content="#FF6B35">
|
||||||
|
<meta property="og:title" content="GrubFlip — Swap Meals Near You">
|
||||||
|
<meta property="og:description" content="The meal trading app for restaurant workers. Swap staff meals, discover new flavors, save money.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/landing.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Nav -->
|
||||||
|
<header class="nav">
|
||||||
|
<div class="nav__inner">
|
||||||
|
<a href="/" class="nav__logo">Grub<span>Flip</span></a>
|
||||||
|
<div class="nav__links">
|
||||||
|
<a href="#how-it-works" class="nav__link">How It Works</a>
|
||||||
|
<a href="#features" class="nav__link hide-mobile">Features</a>
|
||||||
|
<a href="#download" class="btn btn--primary btn--sm">Get the App</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Hero -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero__content">
|
||||||
|
<span class="hero__badge">🍕 Built for restaurant crews</span>
|
||||||
|
<h1 class="hero__title">Tired of the same<br> staff meal?<br><span class="hero__highlight">Flip it.</span></h1>
|
||||||
|
<p class="hero__sub">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.</p>
|
||||||
|
<div class="hero__cta">
|
||||||
|
<a href="#download" class="btn btn--primary btn--lg">Start Flipping</a>
|
||||||
|
<a href="#how-it-works" class="btn btn--outline btn--lg">See How It Works</a>
|
||||||
|
</div>
|
||||||
|
<p class="hero__social-proof">
|
||||||
|
<span class="hero__avatars">
|
||||||
|
<span class="hero__avatar" style="background:#FF6B35;">🧑🍳</span>
|
||||||
|
<span class="hero__avatar" style="background:#2D6A4F;">👩🍳</span>
|
||||||
|
<span class="hero__avatar" style="background:#FFBA08;">👨🍳</span>
|
||||||
|
</span>
|
||||||
|
<span class="hero__social-text"><strong>500+</strong> restaurant workers already flipping</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero__visual">
|
||||||
|
<div class="hero__phone">
|
||||||
|
<div class="phone-mockup">
|
||||||
|
<div class="phone-mockup__screen">
|
||||||
|
<div class="phone-mockup__header">
|
||||||
|
<span class="phone-mockup__logo">Grub<span>Flip</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="phone-mockup__card">
|
||||||
|
<div class="phone-mockup__food-emoji">🍕</div>
|
||||||
|
<div class="phone-mockup__card-text">
|
||||||
|
<strong>Margherita Slice</strong>
|
||||||
|
<small>Marco's Pizzeria · 0.2 mi</small>
|
||||||
|
</div>
|
||||||
|
<span class="phone-mockup__badge">Swap</span>
|
||||||
|
</div>
|
||||||
|
<div class="phone-mockup__card phone-mockup__card--alt">
|
||||||
|
<div class="phone-mockup__food-emoji">🍜</div>
|
||||||
|
<div class="phone-mockup__card-text">
|
||||||
|
<strong>Pad Thai Bowl</strong>
|
||||||
|
<small>Thai Garden · 0.4 mi</small>
|
||||||
|
</div>
|
||||||
|
<span class="phone-mockup__badge phone-mockup__badge--green">Swap</span>
|
||||||
|
</div>
|
||||||
|
<div class="phone-mockup__card">
|
||||||
|
<div class="phone-mockup__food-emoji">🌮</div>
|
||||||
|
<div class="phone-mockup__card-text">
|
||||||
|
<strong>Carnitas Tacos (2x)</strong>
|
||||||
|
<small>La Cocina · 0.3 mi</small>
|
||||||
|
</div>
|
||||||
|
<span class="phone-mockup__badge">Swap</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- How It Works -->
|
||||||
|
<section class="how-it-works" id="how-it-works">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">How It Works</h2>
|
||||||
|
<p class="section-sub">Three steps. That's it. No accounts to verify, no fees, no BS.</p>
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__icon">📸</div>
|
||||||
|
<div class="step__num">1</div>
|
||||||
|
<h3 class="step__title">Post Your Meal</h3>
|
||||||
|
<p class="step__desc">Snap a pic of your staff meal and post it. Add what restaurant you work at and when you're free to swap.</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__icon">🔄</div>
|
||||||
|
<div class="step__num">2</div>
|
||||||
|
<h3 class="step__title">Find a Flip</h3>
|
||||||
|
<p class="step__desc">Browse meals from nearby restaurant workers. See something you want? Send a flip request. They accept, you're in.</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__icon">🤝</div>
|
||||||
|
<div class="step__num">3</div>
|
||||||
|
<h3 class="step__title">Meet & Eat</h3>
|
||||||
|
<p class="step__desc">Meet up between shifts, swap meals, and enjoy something different. Rate the flip and build your rep.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features -->
|
||||||
|
<section class="features" id="features">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Why Restaurant Workers<br>Love GrubFlip</h2>
|
||||||
|
<div class="features__grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__icon">💸</div>
|
||||||
|
<h3 class="feature-card__title">100% Free</h3>
|
||||||
|
<p class="feature-card__desc">No money changes hands. Just meal for meal. The way it should be.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__icon">📍</div>
|
||||||
|
<h3 class="feature-card__title">Hyper-Local</h3>
|
||||||
|
<p class="feature-card__desc">Only see meals within walking distance. Built for your block, not your borough.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__icon">⚡</div>
|
||||||
|
<h3 class="feature-card__title">Shift-Friendly</h3>
|
||||||
|
<p class="feature-card__desc">Set your availability around your shifts. GrubFlip works on your schedule, not the other way around.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__icon">⭐</div>
|
||||||
|
<h3 class="feature-card__title">Rep System</h3>
|
||||||
|
<p class="feature-card__desc">Build your reputation as a reliable flipper. The more you trade, the more people trust you.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Testimonial -->
|
||||||
|
<section class="testimonial">
|
||||||
|
<div class="container">
|
||||||
|
<blockquote class="testimonial__quote">
|
||||||
|
"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."
|
||||||
|
</blockquote>
|
||||||
|
<div class="testimonial__author">
|
||||||
|
<span class="testimonial__avatar">👨🍳</span>
|
||||||
|
<div>
|
||||||
|
<strong>Danny R.</strong>
|
||||||
|
<span>Line cook, Brooklyn</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA / Download -->
|
||||||
|
<section class="download" id="download">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="download__title">Ready to flip?</h2>
|
||||||
|
<p class="download__sub">Join 500+ restaurant workers already trading meals. Available on iOS and Android.</p>
|
||||||
|
<div class="download__buttons">
|
||||||
|
<a href="#" class="btn btn--dark btn--lg download__store-btn" aria-label="Download on the App Store">
|
||||||
|
<svg class="download__store-icon" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
|
||||||
|
App Store
|
||||||
|
</a>
|
||||||
|
<a href="#" class="btn btn--dark btn--lg download__store-btn" aria-label="Get it on Google Play">
|
||||||
|
<svg class="download__store-icon" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M3.18 23.49c-.31-.16-.52-.46-.57-.82L2 3.35c-.04-.36.12-.7.42-.9L13.26 12 2.61 21.55l.57 1.94zM14.62 12L4.5 2.74l10.8 6.3-1.68 2.96zm1.56.89L6.84 21.3l10.76-6.28-1.42-2.13zM21.15 11.1l-3.87-2.27L15.2 12l2.08 3.17 3.87-2.27c.59-.34.59-1.46 0-1.8z"/></svg>
|
||||||
|
Google Play
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="download__note">Or use the <a href="index.html">web app</a> — no download needed.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer__inner">
|
||||||
|
<div class="footer__brand">
|
||||||
|
<span class="footer__logo">Grub<span>Flip</span></span>
|
||||||
|
<p class="footer__tagline">Meal trading for restaurant crews.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer__links">
|
||||||
|
<a href="#">About</a>
|
||||||
|
<a href="#">Privacy</a>
|
||||||
|
<a href="#">Terms</a>
|
||||||
|
<a href="#">Contact</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="footer__copy">© 2026 GrubFlip. Made with 🍕 for the people who make the food.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="js/landing.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
185
messages.html
Normal file
185
messages.html
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GrubFlip — Messages</title>
|
||||||
|
<meta name="theme-color" content="#FF6B35">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
<link rel="stylesheet" href="css/chat.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gf-header">
|
||||||
|
<div class="gf-header__logo">Grub<span>Flip</span></div>
|
||||||
|
<div class="gf-header__actions">
|
||||||
|
<button class="btn-icon" aria-label="New message">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Messages Inbox -->
|
||||||
|
<main class="messages-inbox">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="chat-search">
|
||||||
|
<svg class="chat-search__icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
<input type="text" class="chat-search__input" placeholder="Search conversations..." aria-label="Search conversations">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Trade Context Banner -->
|
||||||
|
<div class="trade-context-banner">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||||
|
<span>3 active trade chats</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conversation List -->
|
||||||
|
<ul class="conversation-list" role="list" aria-label="Conversations">
|
||||||
|
|
||||||
|
<!-- Unread conversation -->
|
||||||
|
<li class="conversation-item conversation-item--unread" role="listitem">
|
||||||
|
<a href="chat.html" class="conversation-item__link">
|
||||||
|
<div class="conversation-item__avatar">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" width="48" height="48">
|
||||||
|
<span class="conversation-item__online" aria-label="Online"></span>
|
||||||
|
</div>
|
||||||
|
<div class="conversation-item__content">
|
||||||
|
<div class="conversation-item__header">
|
||||||
|
<h3 class="conversation-item__name">Marcus J.</h3>
|
||||||
|
<time class="conversation-item__time">2m</time>
|
||||||
|
</div>
|
||||||
|
<p class="conversation-item__preview">Sounds good! I can meet at the corner of 5th and Main around 6pm 👍</p>
|
||||||
|
<div class="conversation-item__meta">
|
||||||
|
<span class="conversation-item__trade-tag">
|
||||||
|
🍕 Margherita → 🍜 Pad Thai
|
||||||
|
</span>
|
||||||
|
<span class="conversation-item__badge" aria-label="2 unread messages">2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Unread conversation -->
|
||||||
|
<li class="conversation-item conversation-item--unread" role="listitem">
|
||||||
|
<a href="chat.html" class="conversation-item__link">
|
||||||
|
<div class="conversation-item__avatar">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" width="48" height="48">
|
||||||
|
</div>
|
||||||
|
<div class="conversation-item__content">
|
||||||
|
<div class="conversation-item__header">
|
||||||
|
<h3 class="conversation-item__name">Priya K.</h3>
|
||||||
|
<time class="conversation-item__time">15m</time>
|
||||||
|
</div>
|
||||||
|
<p class="conversation-item__preview">Hey! Is your butter chicken still available? I've got fresh naan and tikka that I made tonight</p>
|
||||||
|
<div class="conversation-item__meta">
|
||||||
|
<span class="conversation-item__trade-tag">
|
||||||
|
🍗 Butter Chicken → 🫓 Naan & Tikka
|
||||||
|
</span>
|
||||||
|
<span class="conversation-item__badge" aria-label="1 unread message">1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Read conversation -->
|
||||||
|
<li class="conversation-item" role="listitem">
|
||||||
|
<a href="chat.html" class="conversation-item__link">
|
||||||
|
<div class="conversation-item__avatar">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" width="48" height="48">
|
||||||
|
<span class="conversation-item__online" aria-label="Online"></span>
|
||||||
|
</div>
|
||||||
|
<div class="conversation-item__content">
|
||||||
|
<div class="conversation-item__header">
|
||||||
|
<h3 class="conversation-item__name">Alex R.</h3>
|
||||||
|
<time class="conversation-item__time">1h</time>
|
||||||
|
</div>
|
||||||
|
<p class="conversation-item__preview">Trade completed! Thanks for the amazing tacos 🌮🔥</p>
|
||||||
|
<div class="conversation-item__meta">
|
||||||
|
<span class="conversation-item__trade-tag conversation-item__trade-tag--complete">
|
||||||
|
✅ Trade completed
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Read conversation -->
|
||||||
|
<li class="conversation-item" role="listitem">
|
||||||
|
<a href="chat.html" class="conversation-item__link">
|
||||||
|
<div class="conversation-item__avatar">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" width="48" height="48">
|
||||||
|
</div>
|
||||||
|
<div class="conversation-item__content">
|
||||||
|
<div class="conversation-item__header">
|
||||||
|
<h3 class="conversation-item__name">Sofia M.</h3>
|
||||||
|
<time class="conversation-item__time">3h</time>
|
||||||
|
</div>
|
||||||
|
<p class="conversation-item__preview">I'll check if I still have some left after my shift ends at 10</p>
|
||||||
|
<div class="conversation-item__meta">
|
||||||
|
<span class="conversation-item__trade-tag">
|
||||||
|
🥘 Ramen → 🌯 Burrito Bowl
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Read conversation -->
|
||||||
|
<li class="conversation-item" role="listitem">
|
||||||
|
<a href="chat.html" class="conversation-item__link">
|
||||||
|
<div class="conversation-item__avatar">
|
||||||
|
<img src="img/avatar-placeholder.svg" alt="" width="48" height="48">
|
||||||
|
</div>
|
||||||
|
<div class="conversation-item__content">
|
||||||
|
<div class="conversation-item__header">
|
||||||
|
<h3 class="conversation-item__name">Chris T.</h3>
|
||||||
|
<time class="conversation-item__time">Yesterday</time>
|
||||||
|
</div>
|
||||||
|
<p class="conversation-item__preview">No worries, maybe next time!</p>
|
||||||
|
<div class="conversation-item__meta">
|
||||||
|
<span class="conversation-item__trade-tag conversation-item__trade-tag--expired">
|
||||||
|
⏰ Trade expired
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Bottom Nav -->
|
||||||
|
<nav class="gf-bottom-nav" aria-label="Main navigation">
|
||||||
|
<a href="index.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||||
|
Feed
|
||||||
|
</a>
|
||||||
|
<a href="post.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||||
|
Post
|
||||||
|
</a>
|
||||||
|
<a href="messages.html" class="gf-bottom-nav__item active" aria-current="page">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||||||
|
Chat
|
||||||
|
<span class="gf-bottom-nav__badge">3</span>
|
||||||
|
</a>
|
||||||
|
<a href="trades.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||||
|
Trades
|
||||||
|
</a>
|
||||||
|
<button class="gf-bottom-nav__item" aria-label="Profile">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
Profile
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
183
post.html
Normal file
183
post.html
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Post a Meal — GrubFlip</title>
|
||||||
|
<meta name="theme-color" content="#FF6B35">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gf-header">
|
||||||
|
<a href="index.html" class="btn btn--ghost btn--icon" aria-label="Back to feed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||||
|
</a>
|
||||||
|
<h1 style="font-size:1.125rem;font-family:var(--gf-font-heading);">Post a Meal</h1>
|
||||||
|
<div style="width:36px"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Post Form -->
|
||||||
|
<main style="padding:1rem 0 5rem;">
|
||||||
|
<div class="container">
|
||||||
|
<form id="post-meal-form" novalidate>
|
||||||
|
|
||||||
|
<!-- Image Upload -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Photo</label>
|
||||||
|
<div class="upload-zone" id="upload-zone" role="button" tabindex="0" aria-label="Upload meal photo">
|
||||||
|
<div class="upload-zone__prompt">
|
||||||
|
<svg class="upload-zone__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
|
<polyline points="21 15 16 10 5 21"/>
|
||||||
|
</svg>
|
||||||
|
<p class="upload-zone__text">Tap to add a photo</p>
|
||||||
|
<p class="upload-zone__hint">JPG, PNG, or WebP · Max 20MB</p>
|
||||||
|
</div>
|
||||||
|
<div class="upload-zone__preview">
|
||||||
|
<img id="upload-preview-img" src="" alt="Meal preview">
|
||||||
|
<button type="button" class="btn--remove" aria-label="Remove photo">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="image-input" accept="image/jpeg,image/png,image/webp" class="visually-hidden">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="meal-title">What did you make?</label>
|
||||||
|
<input type="text" id="meal-title" class="form-input" placeholder="e.g. Chicken Parm with Penne" maxlength="100" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="meal-description">
|
||||||
|
Description <small>(optional details)</small>
|
||||||
|
</label>
|
||||||
|
<textarea id="meal-description" class="form-textarea" placeholder="Fresh tonight, extra mozzarella. Feeds one." maxlength="300" rows="3"></textarea>
|
||||||
|
<p id="desc-counter" style="text-align:right;font-size:0.75rem;color:var(--gf-neutral-400);margin-top:0.25rem;">0/300</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dietary Tags -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Dietary Tags <small>(select all that apply)</small></label>
|
||||||
|
<div class="tag-picker" role="group" aria-label="Dietary tags">
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-vegan" value="vegan">
|
||||||
|
<label class="tag-picker__label" for="tag-vegan">Vegan</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-vegetarian" value="vegetarian">
|
||||||
|
<label class="tag-picker__label" for="tag-vegetarian">Vegetarian</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-gluten-free" value="gluten-free">
|
||||||
|
<label class="tag-picker__label" for="tag-gluten-free">Gluten-Free</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-dairy" value="dairy">
|
||||||
|
<label class="tag-picker__label" for="tag-dairy">Dairy</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-dairy-free" value="dairy-free">
|
||||||
|
<label class="tag-picker__label" for="tag-dairy-free">Dairy-Free</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-nut-free" value="nut-free">
|
||||||
|
<label class="tag-picker__label" for="tag-nut-free">Nut-Free</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-halal" value="halal">
|
||||||
|
<label class="tag-picker__label" for="tag-halal">Halal</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-kosher" value="kosher">
|
||||||
|
<label class="tag-picker__label" for="tag-kosher">Kosher</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-high-protein" value="high-protein">
|
||||||
|
<label class="tag-picker__label" for="tag-high-protein">High-Protein</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-low-carb" value="low-carb">
|
||||||
|
<label class="tag-picker__label" for="tag-low-carb">Low-Carb</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="tag-picker__option" id="tag-spicy" value="spicy">
|
||||||
|
<label class="tag-picker__label" for="tag-spicy">Spicy 🌶️</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cuisine -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="cuisine-select">Cuisine</label>
|
||||||
|
<select id="cuisine-select" class="form-select">
|
||||||
|
<option value="">Select cuisine type...</option>
|
||||||
|
<option value="Italian">Italian</option>
|
||||||
|
<option value="Mexican">Mexican</option>
|
||||||
|
<option value="Chinese">Chinese</option>
|
||||||
|
<option value="Japanese">Japanese</option>
|
||||||
|
<option value="Indian">Indian</option>
|
||||||
|
<option value="Thai">Thai</option>
|
||||||
|
<option value="Mediterranean">Mediterranean</option>
|
||||||
|
<option value="American">American</option>
|
||||||
|
<option value="Korean">Korean</option>
|
||||||
|
<option value="Vietnamese">Vietnamese</option>
|
||||||
|
<option value="French">French</option>
|
||||||
|
<option value="Middle Eastern">Middle Eastern</option>
|
||||||
|
<option value="Caribbean">Caribbean</option>
|
||||||
|
<option value="Ethiopian">Ethiopian</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Portion + Trade Type Row -->
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="portion-select">Portion</label>
|
||||||
|
<select id="portion-select" class="form-select">
|
||||||
|
<option value="single">Single serving</option>
|
||||||
|
<option value="double">Two servings</option>
|
||||||
|
<option value="family">Family size</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="trade-type-select">Trade Type</label>
|
||||||
|
<select id="trade-type-select" class="form-select">
|
||||||
|
<option value="swap">Swap</option>
|
||||||
|
<option value="gift">Gift (free)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expires -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="expires-select">Available for pickup within</label>
|
||||||
|
<select id="expires-select" class="form-select" required></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<button type="submit" id="submit-btn" class="btn btn--primary btn--lg btn--block">
|
||||||
|
Post Meal
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Bottom Nav -->
|
||||||
|
<nav class="gf-bottom-nav" aria-label="Main navigation">
|
||||||
|
<a href="index.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||||
|
Feed
|
||||||
|
</a>
|
||||||
|
<a href="post.html" class="gf-bottom-nav__item active" aria-current="page">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||||
|
Post
|
||||||
|
</a>
|
||||||
|
<a href="trades.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||||
|
Trades
|
||||||
|
</a>
|
||||||
|
<button class="gf-bottom-nav__item" aria-label="Profile">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
Profile
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
<script src="js/post.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
81
trades.html
Normal file
81
trades.html
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>My Trades — GrubFlip</title>
|
||||||
|
<meta name="theme-color" content="#FF6B35">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gf-header">
|
||||||
|
<div class="gf-header__logo">Grub<span>Flip</span></div>
|
||||||
|
<div class="gf-header__actions">
|
||||||
|
<a href="post.html" class="btn btn--primary btn--sm">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
Post
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Trade Tabs -->
|
||||||
|
<div class="trade-tabs" role="tablist" aria-label="Trade status">
|
||||||
|
<button class="trade-tab active" role="tab" data-tab="active" aria-selected="true">Active</button>
|
||||||
|
<button class="trade-tab" role="tab" data-tab="completed" aria-selected="false">History</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trades List -->
|
||||||
|
<main style="padding:1rem 0 5rem;">
|
||||||
|
<div class="container" id="trades-container">
|
||||||
|
<div id="trades-list" role="feed" aria-label="Trade requests"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Meetup Modal -->
|
||||||
|
<div class="modal-overlay" id="meetup-modal" role="dialog" aria-label="Set meetup details" aria-modal="true">
|
||||||
|
<div class="modal-sheet">
|
||||||
|
<div class="modal-sheet__handle"></div>
|
||||||
|
<h2 style="font-size:1.25rem;margin-bottom:1rem;">Set Meetup Details</h2>
|
||||||
|
<form id="meetup-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="meetup-location">Where to meet?</label>
|
||||||
|
<input type="text" id="meetup-location" class="form-input" placeholder="e.g. Lobby of 123 Main St" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="meetup-time">When? <small>(optional)</small></label>
|
||||||
|
<input type="datetime-local" id="meetup-time" class="form-input">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn--primary btn--lg btn--block">Confirm Meetup</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Nav -->
|
||||||
|
<nav class="gf-bottom-nav" aria-label="Main navigation">
|
||||||
|
<a href="index.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||||
|
Feed
|
||||||
|
</a>
|
||||||
|
<a href="post.html" class="gf-bottom-nav__item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||||
|
Post
|
||||||
|
</a>
|
||||||
|
<a href="trades.html" class="gf-bottom-nav__item active" aria-current="page" style="position:relative">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4m0 0l4-4m-4 4h18M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||||
|
Trades
|
||||||
|
</a>
|
||||||
|
<button class="gf-bottom-nav__item" aria-label="Profile">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
Profile
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
<script src="js/trades.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue