add image-first directory card redesign and cover image upload
- 4-tier visual ladder: Free (muted grey) → Basic (verified presence) → Growth (stats + quotes) → Pro (court media + full stats + green glow) - New card layout: 16:9 cover media, frosted category badge, logo avatar straddling media/body border (body-relative to avoid overflow:hidden clip) - Pro default: CSS court visualization placeholder; Growth/Basic: dark-green grid placeholder; Free: grey/desaturated placeholder - Cover image upload in supplier dashboard (saves to static/uploads/covers/) - Migration 0013: cover_image TEXT column on suppliers table - Updated prototype (scratch/design_directory_cards.html) with Basic tier cards, fixed logo-wrap positioning across all tiers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- Redesigned directory cards: image-first 16:9 layout with cover photos, frosted category badge, CSS court/grid placeholders, and logo avatar straddling the media/body border
|
||||
- 4-tier visual ladder: Free (62% opacity, grey placeholder, unverified chip) → Basic (full opacity, green placeholder, verified chip, description, "View Listing →") → Growth (green placeholder, Growth chip + stats, "Request Quote →") → Pro (green 2.5px top border, CSS court media, full stats, green hover glow)
|
||||
- Pro card media defaults to CSS court visualization when no cover image; Growth/Basic default to dark-green grid placeholder
|
||||
|
||||
### Added
|
||||
- Cover image upload for suppliers in the dashboard listing form (saves to `static/uploads/covers/`, 16:9 thumbnail preview)
|
||||
- Migration 0013: `cover_image TEXT` column on suppliers table
|
||||
|
||||
### Added
|
||||
- **Basic subscription tier** — verified directory listing with contact info, services checklist, social links, and enquiry form; no lead credits
|
||||
- **Monthly + yearly billing** — all paid supplier tiers now offer yearly pricing with annual discount (Basic: €349/yr, Growth: €1,799/yr, Pro: €4,499/yr)
|
||||
|
||||
@@ -5,6 +5,22 @@
|
||||
{% block head %}
|
||||
<meta name="description" content="Browse {{ total_suppliers }}+ padel court suppliers across {{ total_countries }} countries. Manufacturers, builders, turf, lighting, and software. Find the right partner for your project.">
|
||||
<style>
|
||||
:root {
|
||||
--dir-green: #15803D;
|
||||
--dir-green-mid: #16a34a;
|
||||
--dir-green-dark: #0D5228;
|
||||
--dir-blue: #1D4ED8;
|
||||
--dir-text-1: #111827;
|
||||
--dir-text-2: #6B7280;
|
||||
--dir-text-3: #9CA3AF;
|
||||
--dir-card-bg: #FFFFFF;
|
||||
--dir-card-bdr: #E6E2D8;
|
||||
--dir-r: 16px;
|
||||
--dir-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.04);
|
||||
--dir-shadow-hov: 0 14px 36px rgba(0,0,0,0.10), 0 2px 8px rgba(0,0,0,0.06);
|
||||
--dir-shadow-pro: 0 14px 36px rgba(21,128,61,0.13), 0 2px 8px rgba(21,128,61,0.08);
|
||||
}
|
||||
|
||||
.dir-hero { text-align: center; padding: 2rem 0 1.5rem; }
|
||||
.dir-hero h1 { font-size: 2rem; line-height: 1.3; }
|
||||
.dir-hero p { color: #64748B; margin-top: 0.5rem; }
|
||||
@@ -36,94 +52,226 @@
|
||||
.dir-filter-tag a { color: #1D4ED8; text-decoration: none; font-weight: 700; }
|
||||
|
||||
.dir-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem; margin-bottom: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.25rem; margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Base card */
|
||||
/* ---- Base card ---- */
|
||||
.dir-card {
|
||||
background: white; border: 1px solid #E2E8F0; border-radius: 14px;
|
||||
padding: 1.25rem; transition: box-shadow 0.15s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
background: var(--dir-card-bg);
|
||||
border: 1px solid var(--dir-card-bdr);
|
||||
border-radius: var(--dir-r);
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
position: relative;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: var(--dir-shadow);
|
||||
}
|
||||
.dir-card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
|
||||
.dir-card__head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
|
||||
.dir-card__name { font-size: 0.9375rem; font-weight: 600; color: #1E293B; margin: 0; }
|
||||
.dir-card__badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||
font-size: 0.6875rem; font-weight: 600; white-space: nowrap;
|
||||
background: #F1F5F9; color: #475569;
|
||||
.dir-card:hover { transform: translateY(-3px); box-shadow: var(--dir-shadow-hov); }
|
||||
|
||||
/* ---- Cover media ---- */
|
||||
.dir-card__media {
|
||||
position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden;
|
||||
}
|
||||
.dir-card__badge--manufacturer { background: #DBEAFE; color: #1D4ED8; }
|
||||
.dir-card__badge--turnkey { background: #D1FAE5; color: #065F46; }
|
||||
.dir-card__badge--turf { background: #FEF3C7; color: #92400E; }
|
||||
.dir-card__badge--lighting { background: #FEE2E2; color: #991B1B; }
|
||||
.dir-card__badge--software { background: #EDE9FE; color: #5B21B6; }
|
||||
.dir-card__badge--hall_builder { background: #FFEDD5; color: #9A3412; }
|
||||
.dir-card__badge--consultant { background: #F0FDFA; color: #134E4A; }
|
||||
.dir-card__badge--franchise { background: #FCE7F3; color: #9D174D; }
|
||||
.dir-card__loc { font-size: 0.8125rem; color: #64748B; margin-top: 4px; }
|
||||
.dir-card__desc {
|
||||
font-size: 0.8125rem; color: #475569; margin-top: 0.5rem;
|
||||
display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
|
||||
.dir-card__media img {
|
||||
width: 100%; height: 100%; object-fit: cover; display: block;
|
||||
transition: transform 0.45s ease;
|
||||
}
|
||||
.dir-card__desc--3line { -webkit-line-clamp: 3; }
|
||||
.dir-card__foot { display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem; font-size: 0.75rem; }
|
||||
.dir-card__web a { color: #1D4ED8; text-decoration: none; }
|
||||
.dir-card__web a:hover { text-decoration: underline; }
|
||||
.dir-card__claim { color: #94A3B8; }
|
||||
.dir-card__claim a { color: #1D4ED8; text-decoration: none; font-weight: 500; }
|
||||
.dir-card__logo { width: 32px; height: 32px; border-radius: 6px; object-fit: contain; }
|
||||
.dir-card__logo-placeholder {
|
||||
width: 32px; height: 32px; border-radius: 6px;
|
||||
background: #EFF6FF; color: #1D4ED8; font-size: 0.6rem; font-weight: 700;
|
||||
.dir-card:hover .dir-card__media img { transform: scale(1.04); }
|
||||
.dir-card__media::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 45%, rgba(0,0,0,0.45) 100%);
|
||||
pointer-events: none; z-index: 2;
|
||||
}
|
||||
|
||||
/* CSS court visualization (pro default placeholder) */
|
||||
.dir-card__media--court {
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 20%, rgba(74,222,128,0.15) 0%, transparent 60%),
|
||||
linear-gradient(160deg, #1a5c30 0%, #2d7a45 35%, #1e6338 65%, #143d22 100%);
|
||||
}
|
||||
.dir-card__media--court .court-lines {
|
||||
position: absolute; inset: 0; z-index: 1;
|
||||
background-image:
|
||||
linear-gradient(transparent calc(49.5% - 1px), rgba(255,255,255,0.92) calc(49.5% - 1px), rgba(255,255,255,0.92) calc(50.5% + 1px), transparent calc(50.5% + 1px)),
|
||||
linear-gradient(transparent calc(8% - 0.5px), rgba(255,255,255,0.65) calc(8% - 0.5px), rgba(255,255,255,0.65) calc(8% + 0.75px), transparent calc(8% + 0.75px)),
|
||||
linear-gradient(transparent calc(92% - 0.5px), rgba(255,255,255,0.65) calc(92% - 0.5px), rgba(255,255,255,0.65) calc(92% + 0.75px), transparent calc(92% + 0.75px)),
|
||||
linear-gradient(90deg, transparent calc(10% - 0.5px), rgba(255,255,255,0.55) calc(10% - 0.5px), rgba(255,255,255,0.55) calc(10% + 0.75px), transparent calc(10% + 0.75px)),
|
||||
linear-gradient(90deg, transparent calc(90% - 0.5px), rgba(255,255,255,0.55) calc(90% - 0.5px), rgba(255,255,255,0.55) calc(90% + 0.75px), transparent calc(90% + 0.75px));
|
||||
}
|
||||
.dir-card__media--court .court-lines::before {
|
||||
content: ''; position: absolute; left: 10%; right: 10%; top: calc(50% - 1.5px); height: 3px;
|
||||
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.18) 3px, transparent 3px, transparent 8px);
|
||||
z-index: 2;
|
||||
}
|
||||
.dir-card__media--court .court-lines::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background-image: repeating-linear-gradient(0deg, transparent, transparent 5px, rgba(0,0,0,0.015) 5px, rgba(0,0,0,0.015) 6px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Dark-green grid placeholder (paid tiers without cover photo) */
|
||||
.dir-card__media--placeholder {
|
||||
background: linear-gradient(135deg, #0f2218 0%, #1a3828 50%, #0d1e14 100%);
|
||||
}
|
||||
.dir-card__media--placeholder .ph-grid {
|
||||
position: absolute; inset: 0; z-index: 1;
|
||||
background-image:
|
||||
repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(255,255,255,0.04) 39px, rgba(255,255,255,0.04) 40px),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(255,255,255,0.04) 39px, rgba(255,255,255,0.04) 40px);
|
||||
}
|
||||
.dir-card__media--placeholder .ph-label {
|
||||
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
||||
z-index: 2; font-size: 1.625rem; font-weight: 800;
|
||||
color: rgba(255,255,255,0.07); letter-spacing: -0.02em; text-transform: uppercase;
|
||||
}
|
||||
/* Free tier placeholder — grey/desaturated */
|
||||
.dir-card--free .dir-card__media--placeholder {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #374151 50%, #1f2937 100%);
|
||||
}
|
||||
|
||||
/* Example card media */
|
||||
.dir-card__media--example {
|
||||
background: linear-gradient(135deg, #1e2e4a 0%, #2d4270 50%, #1a273e 100%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
/* Tier badges */
|
||||
.dir-card__tier-badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||
font-size: 0.625rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; margin-top: 4px;
|
||||
.dir-card__media--example .example-icon {
|
||||
text-align: center; z-index: 3; position: relative;
|
||||
}
|
||||
.dir-card__tier-badge--verified { background: #D1FAE5; color: #065F46; }
|
||||
.dir-card__tier-badge--growth { background: #DBEAFE; color: #1D4ED8; }
|
||||
.dir-card__tier-badge--unverified { background: #F1F5F9; color: #94A3B8; }
|
||||
|
||||
/* Free card — muted */
|
||||
.dir-card--free {
|
||||
opacity: 0.7; background: #FAFBFC;
|
||||
.dir-card__media--example .example-icon svg { opacity: 0.25; }
|
||||
.dir-card__media--example .example-icon p {
|
||||
margin-top: 6px; font-size: 0.6875rem; font-weight: 600;
|
||||
color: rgba(255,255,255,0.25); letter-spacing: 0.04em;
|
||||
}
|
||||
.dir-card--free:hover { opacity: 0.85; }
|
||||
|
||||
/* Pro card — green accent */
|
||||
/* ---- Overlays ---- */
|
||||
/* Category badge — frosted pill, top-right of media */
|
||||
.dir-card__cat {
|
||||
position: absolute; top: 10px; right: 10px; z-index: 5;
|
||||
font-size: 0.6375rem; font-weight: 700; letter-spacing: 0.03em;
|
||||
text-transform: uppercase; padding: 3.5px 9px; border-radius: 5px;
|
||||
backdrop-filter: blur(8px) saturate(180%); -webkit-backdrop-filter: blur(8px) saturate(180%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dir-card__cat--manufacturer { background: rgba(219,234,254,0.92); color: #1e40af; }
|
||||
.dir-card__cat--turnkey { background: rgba(220,252,231,0.92); color: #065f46; }
|
||||
.dir-card__cat--turf { background: rgba(254,243,199,0.92); color: #78350f; }
|
||||
.dir-card__cat--lighting { background: rgba(254,226,226,0.92); color: #991b1b; }
|
||||
.dir-card__cat--software { background: rgba(237,233,254,0.92); color: #4c1d95; }
|
||||
.dir-card__cat--hall_builder { background: rgba(255,237,213,0.92); color: #7c2d12; }
|
||||
.dir-card__cat--consultant { background: rgba(240,253,250,0.92); color: #134e4a; }
|
||||
.dir-card__cat--franchise { background: rgba(252,231,243,0.92); color: #831843; }
|
||||
.dir-card__cat--example { background: rgba(219,234,254,0.85); color: #1e40af; }
|
||||
|
||||
/* Featured ribbon — top-left */
|
||||
.dir-card__featured {
|
||||
position: absolute; top: 10px; left: 10px; z-index: 5;
|
||||
background: var(--dir-blue); color: white;
|
||||
font-size: 0.5875rem; font-weight: 700; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; padding: 3.5px 9px; border-radius: 5px;
|
||||
box-shadow: 0 2px 8px rgba(29,78,216,0.35);
|
||||
}
|
||||
|
||||
/* Logo avatar — body-relative so it straddles the media/body border */
|
||||
.dir-card__logo-wrap {
|
||||
position: absolute; top: -22px; left: 14px; z-index: 6;
|
||||
}
|
||||
.dir-card__logo,
|
||||
.dir-card__logo-ph {
|
||||
width: 44px; height: 44px; border-radius: 10px;
|
||||
border: 2.5px solid white; box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.dir-card__logo { object-fit: contain; background: white; }
|
||||
.dir-card__logo-ph {
|
||||
background: #EFF6FF; color: var(--dir-blue);
|
||||
font-size: 0.875rem; font-weight: 800; letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ---- Card body ---- */
|
||||
.dir-card__body {
|
||||
position: relative;
|
||||
padding: 30px 16px 16px; /* 30px top = logo overhang room */
|
||||
}
|
||||
.dir-card__name {
|
||||
font-size: 0.9375rem; font-weight: 700; color: var(--dir-text-1);
|
||||
line-height: 1.25; letter-spacing: -0.01em;
|
||||
}
|
||||
.dir-card__loc {
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
font-size: 0.75rem; color: var(--dir-text-2); margin-top: 3px;
|
||||
}
|
||||
.dir-card__loc svg { flex-shrink: 0; }
|
||||
.dir-card__stats {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
|
||||
margin-top: 10px; padding-top: 10px; border-top: 1px solid #F1F5F9;
|
||||
}
|
||||
.dir-card__stat {
|
||||
display: flex; align-items: center; gap: 3.5px;
|
||||
font-size: 0.6875rem; font-weight: 600; color: var(--dir-text-2); white-space: nowrap;
|
||||
}
|
||||
.dir-card__stat svg { flex-shrink: 0; color: var(--dir-text-3); }
|
||||
.dir-card__stat--verified { color: var(--dir-green); }
|
||||
.dir-card__stat--verified svg { color: var(--dir-green); }
|
||||
.dir-card__tier-chip {
|
||||
font-size: 0.5875rem; font-weight: 700; letter-spacing: 0.05em;
|
||||
text-transform: uppercase; padding: 2.5px 7px; border-radius: 4px;
|
||||
}
|
||||
.dir-card__tier-chip--growth { background: #EFF6FF; color: #1e40af; }
|
||||
.dir-card__tier-chip--unverified { background: #F9FAFB; color: var(--dir-text-3); }
|
||||
.dir-card__desc {
|
||||
font-size: 0.8125rem; color: var(--dir-text-2); line-height: 1.55; margin-top: 8px;
|
||||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
|
||||
}
|
||||
.dir-card__foot {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-top: 12px; padding-top: 12px; border-top: 1px solid #F1F5F9;
|
||||
}
|
||||
.dir-card__web {
|
||||
font-size: 0.75rem; color: var(--dir-text-3);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 170px;
|
||||
}
|
||||
.dir-card__action {
|
||||
font-size: 0.75rem; font-weight: 700; letter-spacing: 0.01em;
|
||||
white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.dir-card__action--quote { color: var(--dir-green); }
|
||||
.dir-card__action--claim { color: var(--dir-text-3); }
|
||||
.dir-card__action--example { color: var(--dir-blue); }
|
||||
|
||||
/* ---- Tier variants ---- */
|
||||
/* PRO — green top line + subtle green glow */
|
||||
.dir-card--pro::before {
|
||||
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2.5px;
|
||||
background: linear-gradient(90deg, var(--dir-green-dark), var(--dir-green-mid));
|
||||
z-index: 7; border-radius: var(--dir-r) var(--dir-r) 0 0;
|
||||
}
|
||||
.dir-card--pro {
|
||||
border-color: #16A34A; border-width: 1.5px;
|
||||
box-shadow: 0 1px 3px rgba(21,128,61,0.06), 0 1px 2px rgba(0,0,0,0.04);
|
||||
}
|
||||
.dir-card--pro:hover { box-shadow: var(--dir-shadow-pro); }
|
||||
|
||||
/* Highlight glow */
|
||||
/* HIGHLIGHT — green ring (boost add-on) */
|
||||
.dir-card--highlight {
|
||||
box-shadow: 0 0 0 2px rgba(22,163,74,0.15), 0 4px 20px rgba(22,163,74,0.1);
|
||||
box-shadow: 0 0 0 2px rgba(21,128,61,0.2), 0 6px 24px rgba(21,128,61,0.1);
|
||||
}
|
||||
.dir-card--highlight:hover {
|
||||
box-shadow: 0 0 0 2px rgba(21,128,61,0.3), var(--dir-shadow-pro);
|
||||
}
|
||||
|
||||
/* Sticky — blue top border */
|
||||
.dir-card--sticky {
|
||||
border-top: 3px solid #1D4ED8;
|
||||
/* FREE — muted */
|
||||
.dir-card--free { opacity: 0.62; }
|
||||
.dir-card--free:hover {
|
||||
opacity: 0.82; transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Featured badge */
|
||||
.dir-card__featured-badge {
|
||||
position: absolute; top: -1px; right: 12px;
|
||||
background: #1D4ED8; color: white; font-size: 0.625rem;
|
||||
font-weight: 700; padding: 3px 8px; border-radius: 0 0 6px 6px;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Example card */
|
||||
.dir-card--example {
|
||||
border: 2px dashed #93C5FD; background: #EFF6FF;
|
||||
}
|
||||
/* EXAMPLE — dashed CTA card */
|
||||
.dir-card--example { border: 2px dashed #93C5FD; background: #F0F7FF; }
|
||||
.dir-card--example::before { display: none; }
|
||||
|
||||
.dir-pagination { display: flex; justify-content: center; gap: 4px; margin: 2rem 0; }
|
||||
.dir-pagination a, .dir-pagination span {
|
||||
|
||||
@@ -3,112 +3,270 @@
|
||||
{% if page > 1 %} (page {{ page }}){% endif %}
|
||||
</p>
|
||||
|
||||
{# Category abbrev for placeholder label #}
|
||||
{% set ph_labels = {
|
||||
'manufacturer': 'Mfr',
|
||||
'turnkey': 'Turn',
|
||||
'turf': 'Turf',
|
||||
'lighting': 'Lgts',
|
||||
'software': 'Sftw',
|
||||
'hall_builder': 'Hall',
|
||||
'consultant': 'Cons',
|
||||
'franchise': 'Frch',
|
||||
'industry_body': 'Ind',
|
||||
} %}
|
||||
|
||||
{# Logo placeholder background/color by category #}
|
||||
{% set logo_ph_styles = {
|
||||
'manufacturer': 'background:#DBEAFE;color:#1e40af',
|
||||
'turnkey': 'background:#D1FAE5;color:#065f46',
|
||||
'turf': 'background:#FEF3C7;color:#78350f',
|
||||
'lighting': 'background:#FEE2E2;color:#991b1b',
|
||||
'software': 'background:#EDE9FE;color:#4c1d95',
|
||||
'hall_builder': 'background:#FFEDD5;color:#7c2d12',
|
||||
'consultant': 'background:#CCFBF1;color:#134e4a',
|
||||
'franchise': 'background:#FCE7F3;color:#831843',
|
||||
} %}
|
||||
|
||||
{% if suppliers %}
|
||||
<div class="dir-grid">
|
||||
|
||||
{# Example card on first page with no active filters #}
|
||||
{% if page == 1 and not q and not country and not category %}
|
||||
<div class="dir-card dir-card--example">
|
||||
<div class="dir-card__featured-badge">Example</div>
|
||||
<div class="dir-card__head">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<div class="dir-card__logo-placeholder">Logo</div>
|
||||
<a href="{{ url_for('public.suppliers') }}" class="dir-card dir-card--example">
|
||||
<div class="dir-card__media dir-card__media--example">
|
||||
<div class="ph-grid"></div>
|
||||
<div class="example-icon">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
<p>Your project photo</p>
|
||||
</div>
|
||||
<div class="dir-card__featured" style="background:#3B82F6">Example</div>
|
||||
<div class="dir-card__cat dir-card__cat--example">Your Category</div>
|
||||
</div>
|
||||
<div class="dir-card__body">
|
||||
<div class="dir-card__logo-wrap">
|
||||
<div class="dir-card__logo-ph">?</div>
|
||||
</div>
|
||||
<h3 class="dir-card__name">Your Company</h3>
|
||||
</div>
|
||||
<span class="dir-card__badge dir-card__badge--manufacturer">Manufacturer</span>
|
||||
</div>
|
||||
<p class="dir-card__loc">Your City, Your Country</p>
|
||||
<div class="dir-card__tier-badge dir-card__tier-badge--verified">Verified ✓</div>
|
||||
<p class="dir-card__desc">Full description of your company, services, certifications, and experience. Pro listings get maximum visibility with logo, website, and priority placement.</p>
|
||||
<div class="dir-card__foot">
|
||||
<span class="dir-card__web"><a href="{{ url_for('public.suppliers') }}">yourcompany.com</a></span>
|
||||
</div>
|
||||
<p style="text-align:center;font-size:0.75rem;color:#1D4ED8;margin-top:0.5rem;font-weight:600">
|
||||
<a href="{{ url_for('public.suppliers') }}" style="color:#1D4ED8;text-decoration:none">Your listing could look like this →</a>
|
||||
<p class="dir-card__loc">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
||||
Your City, Country
|
||||
</p>
|
||||
<div class="dir-card__stats">
|
||||
<span class="dir-card__stat dir-card__stat--verified">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
Verified
|
||||
</span>
|
||||
<span class="dir-card__stat">12 projects · 8 yrs</span>
|
||||
</div>
|
||||
<p class="dir-card__desc">Verified listings include cover photo, project stats, and a direct quote button — placed above unverified suppliers in search results.</p>
|
||||
<div class="dir-card__foot">
|
||||
<span></span>
|
||||
<span class="dir-card__action dir-card__action--example">Get listed →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for s in suppliers %}
|
||||
{# --- Pro tier card --- #}
|
||||
{% set logo_ph_style = logo_ph_styles.get(s.category, 'background:#EFF6FF;color:#1e40af') %}
|
||||
{% set ph_label = ph_labels.get(s.category, s.category[:4] | upper) %}
|
||||
|
||||
{# ---- Pro tier ---- #}
|
||||
{% if s.tier == 'pro' %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}" class="dir-card dir-card--pro {% if s.sticky_until and s.sticky_until > now %}dir-card--sticky{% endif %} {% if s.highlight %}dir-card--highlight{% endif %}" style="text-decoration:none;color:inherit;display:block{% if card_colors.get(s.id) %};border-color:{{ card_colors[s.id] }};border-width:2px{% endif %}">
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured-badge">Featured</div>{% endif %}
|
||||
<div class="dir-card__head">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
{% if s.logo_file or s.logo_url %}<img src="{{ s.logo_file or s.logo_url }}" alt="" class="dir-card__logo">{% endif %}
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}"
|
||||
class="dir-card dir-card--pro{% if s.sticky_until and s.sticky_until > now %} dir-card--sticky{% endif %}{% if s.highlight %} dir-card--highlight{% endif %}"{% if card_colors.get(s.id) %} style="border-color:{{ card_colors[s.id] }}"{% endif %}>
|
||||
{% if s.cover_image %}
|
||||
<div class="dir-card__media">
|
||||
<img src="{{ s.cover_image }}" alt="{{ s.name }}">
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured">Featured</div>{% endif %}
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
<span class="dir-card__badge dir-card__badge--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</span>
|
||||
</div>
|
||||
<p class="dir-card__loc">{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}</p>
|
||||
<div class="dir-card__tier-badge dir-card__tier-badge--verified">Verified ✓</div>
|
||||
{% if s.description %}
|
||||
<p class="dir-card__desc">{{ s.description }}</p>
|
||||
{% endif %}
|
||||
<div class="dir-card__foot">
|
||||
<span class="dir-card__web">
|
||||
{% if s.website %}{{ s.website }}{% endif %}
|
||||
</span>
|
||||
<span style="font-size:0.6875rem;color:#1D4ED8;font-weight:600">Request Quote →</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# --- Growth tier card --- #}
|
||||
{% elif s.tier == 'growth' %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}" class="dir-card dir-card--growth {% if s.sticky_until and s.sticky_until > now %}dir-card--sticky{% endif %}" style="text-decoration:none;color:inherit;display:block{% if card_colors.get(s.id) %};border-color:{{ card_colors[s.id] }};border-width:2px{% endif %}">
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured-badge">Featured</div>{% endif %}
|
||||
<div class="dir-card__head">
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<span class="dir-card__badge dir-card__badge--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</span>
|
||||
</div>
|
||||
<p class="dir-card__loc">{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}</p>
|
||||
<div class="dir-card__tier-badge dir-card__tier-badge--growth">Growth</div>
|
||||
{% if s.description %}
|
||||
<p class="dir-card__desc dir-card__desc--3line">{{ s.description }}</p>
|
||||
{% endif %}
|
||||
<div class="dir-card__foot">
|
||||
<span></span>
|
||||
<span style="font-size:0.6875rem;color:#1D4ED8;font-weight:600">Request Quote →</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# --- Basic tier card --- #}
|
||||
{% elif s.tier == 'basic' %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}" class="dir-card dir-card--basic {% if s.sticky_until and s.sticky_until > now %}dir-card--sticky{% endif %}" style="text-decoration:none;color:inherit;display:block">
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured-badge">Featured</div>{% endif %}
|
||||
<div class="dir-card__head">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
{% if s.logo_file or s.logo_url %}<img src="{{ s.logo_file or s.logo_url }}" alt="" class="dir-card__logo">{% endif %}
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
</div>
|
||||
<span class="dir-card__badge dir-card__badge--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</span>
|
||||
</div>
|
||||
<p class="dir-card__loc">{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}</p>
|
||||
<div class="dir-card__tier-badge dir-card__tier-badge--verified">Verified ✓</div>
|
||||
{% if s.short_description or s.description %}
|
||||
<p class="dir-card__desc dir-card__desc--3line">{{ s.short_description or s.description }}</p>
|
||||
{% endif %}
|
||||
<div class="dir-card__foot">
|
||||
<span class="dir-card__web">{% if s.website %}{{ s.website }}{% endif %}</span>
|
||||
<span style="font-size:0.6875rem;color:#64748B;font-weight:600">View Listing →</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# --- Free / unclaimed tier card --- #}
|
||||
{% else %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}" class="dir-card dir-card--free" style="text-decoration:none;color:inherit;display:block">
|
||||
<div class="dir-card__head">
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<span class="dir-card__badge dir-card__badge--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</span>
|
||||
<div class="dir-card__media dir-card__media--court">
|
||||
<div class="court-lines"></div>
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured">Featured</div>{% endif %}
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dir-card__body">
|
||||
<div class="dir-card__logo-wrap">
|
||||
{% if s.logo_file or s.logo_url %}
|
||||
<img src="{{ s.logo_file or s.logo_url }}" alt="" class="dir-card__logo">
|
||||
{% else %}
|
||||
<div class="dir-card__logo-ph" style="{{ logo_ph_style }}">{{ s.name[0] | upper }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<p class="dir-card__loc">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
||||
{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}
|
||||
</p>
|
||||
<div class="dir-card__stats">
|
||||
<span class="dir-card__stat dir-card__stat--verified">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
Verified
|
||||
</span>
|
||||
{% if s.project_count %}
|
||||
<span class="dir-card__stat">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
|
||||
{{ s.project_count }} projects
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if s.years_in_business %}
|
||||
<span class="dir-card__stat">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{ s.years_in_business }} yrs
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if s.service_area %}
|
||||
<span class="dir-card__stat">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
|
||||
{{ s.service_area.split(',')[0] }}{% if s.service_area.split(',')|length > 1 %} +{{ s.service_area.split(',')|length - 1 }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if s.short_description or s.description %}
|
||||
<p class="dir-card__desc">{{ s.short_description or s.description }}</p>
|
||||
{% endif %}
|
||||
<div class="dir-card__foot">
|
||||
<span class="dir-card__web">{{ s.website or '' }}</span>
|
||||
<span class="dir-card__action dir-card__action--quote">Request Quote →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# ---- Growth tier ---- #}
|
||||
{% elif s.tier == 'growth' %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}"
|
||||
class="dir-card{% if s.sticky_until and s.sticky_until > now %} dir-card--sticky{% endif %}"{% if card_colors.get(s.id) %} style="border-color:{{ card_colors[s.id] }}"{% endif %}>
|
||||
{% if s.cover_image %}
|
||||
<div class="dir-card__media">
|
||||
<img src="{{ s.cover_image }}" alt="{{ s.name }}">
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured">Featured</div>{% endif %}
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="dir-card__media dir-card__media--placeholder">
|
||||
<div class="ph-grid"></div>
|
||||
<div class="ph-label">{{ ph_label }}</div>
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured">Featured</div>{% endif %}
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dir-card__body">
|
||||
<div class="dir-card__logo-wrap">
|
||||
{% if s.logo_file or s.logo_url %}
|
||||
<img src="{{ s.logo_file or s.logo_url }}" alt="" class="dir-card__logo">
|
||||
{% else %}
|
||||
<div class="dir-card__logo-ph" style="{{ logo_ph_style }}">{{ s.name[0] | upper }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<p class="dir-card__loc">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
||||
{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}
|
||||
</p>
|
||||
<div class="dir-card__stats">
|
||||
<span class="dir-card__tier-chip dir-card__tier-chip--growth">Growth</span>
|
||||
{% if s.project_count %}
|
||||
<span class="dir-card__stat">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
|
||||
{{ s.project_count }} projects
|
||||
</span>
|
||||
{% elif s.years_in_business %}
|
||||
<span class="dir-card__stat">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{ s.years_in_business }} yrs
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if s.short_description or s.description %}
|
||||
<p class="dir-card__desc">{{ s.short_description or s.description }}</p>
|
||||
{% endif %}
|
||||
<div class="dir-card__foot">
|
||||
<span class="dir-card__web">{{ s.website or '' }}</span>
|
||||
<span class="dir-card__action dir-card__action--quote">Request Quote →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# ---- Basic tier ---- #}
|
||||
{% elif s.tier == 'basic' %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}"
|
||||
class="dir-card{% if s.sticky_until and s.sticky_until > now %} dir-card--sticky{% endif %}">
|
||||
{% if s.cover_image %}
|
||||
<div class="dir-card__media">
|
||||
<img src="{{ s.cover_image }}" alt="{{ s.name }}">
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured">Featured</div>{% endif %}
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="dir-card__media dir-card__media--placeholder">
|
||||
<div class="ph-grid"></div>
|
||||
<div class="ph-label">{{ ph_label }}</div>
|
||||
{% if s.sticky_until and s.sticky_until > now %}<div class="dir-card__featured">Featured</div>{% endif %}
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dir-card__body">
|
||||
<div class="dir-card__logo-wrap">
|
||||
{% if s.logo_file or s.logo_url %}
|
||||
<img src="{{ s.logo_file or s.logo_url }}" alt="" class="dir-card__logo">
|
||||
{% else %}
|
||||
<div class="dir-card__logo-ph" style="{{ logo_ph_style }}">{{ s.name[0] | upper }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<p class="dir-card__loc">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
||||
{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}
|
||||
</p>
|
||||
<div class="dir-card__stats">
|
||||
<span class="dir-card__stat dir-card__stat--verified">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
Verified
|
||||
</span>
|
||||
</div>
|
||||
{% if s.short_description or s.description %}
|
||||
<p class="dir-card__desc">{{ s.short_description or s.description }}</p>
|
||||
{% endif %}
|
||||
<div class="dir-card__foot">
|
||||
<span class="dir-card__web">{{ s.website or '' }}</span>
|
||||
<span class="dir-card__action dir-card__action--claim">View Listing →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# ---- Free / unclaimed tier ---- #}
|
||||
{% else %}
|
||||
<a href="{{ url_for('directory.supplier_detail', slug=s.slug) }}"
|
||||
class="dir-card dir-card--free">
|
||||
<div class="dir-card__media dir-card__media--placeholder">
|
||||
<div class="ph-grid"></div>
|
||||
<div class="ph-label">{{ ph_label }}</div>
|
||||
<div class="dir-card__cat dir-card__cat--{{ s.category }}">{{ category_labels.get(s.category, s.category) }}</div>
|
||||
</div>
|
||||
<div class="dir-card__body">
|
||||
<div class="dir-card__logo-wrap">
|
||||
<div class="dir-card__logo-ph" style="{{ logo_ph_style }}">{{ s.name[0] | upper }}</div>
|
||||
</div>
|
||||
<h3 class="dir-card__name">{{ s.name }}</h3>
|
||||
<p class="dir-card__loc">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
||||
{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}
|
||||
</p>
|
||||
<div class="dir-card__stats">
|
||||
<span class="dir-card__tier-chip dir-card__tier-chip--unverified">Unverified</span>
|
||||
</div>
|
||||
<p class="dir-card__loc">{{ country_labels.get(s.country_code, s.country_code) }}{% if s.city %}, {{ s.city }}{% endif %}</p>
|
||||
<div class="dir-card__tier-badge dir-card__tier-badge--unverified">Unverified</div>
|
||||
<div class="dir-card__foot">
|
||||
<span></span>
|
||||
<span class="dir-card__claim">Is this your company? →</span>
|
||||
<span class="dir-card__action dir-card__action--claim">Is this yours? →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -223,7 +223,10 @@ def up(conn):
|
||||
contact_role TEXT,
|
||||
linkedin_url TEXT,
|
||||
instagram_url TEXT,
|
||||
youtube_url TEXT
|
||||
youtube_url TEXT,
|
||||
|
||||
-- Phase 4: Directory card cover image
|
||||
cover_image TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_suppliers_country ON suppliers(country_code);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Add cover_image column to suppliers table."""
|
||||
|
||||
|
||||
def up(conn):
|
||||
cols = [r[1] for r in conn.execute("PRAGMA table_info(suppliers)").fetchall()]
|
||||
if "cover_image" not in cols:
|
||||
conn.execute("ALTER TABLE suppliers ADD COLUMN cover_image TEXT")
|
||||
conn.commit()
|
||||
@@ -712,7 +712,7 @@ async def dashboard_listing_save():
|
||||
supplier = g.supplier
|
||||
form = await request.form
|
||||
|
||||
# Handle file upload
|
||||
# Handle file uploads
|
||||
files = await request.files
|
||||
logo_file = files.get("logo_file")
|
||||
logo_path = supplier.get("logo_file") or ""
|
||||
@@ -724,6 +724,16 @@ async def dashboard_listing_save():
|
||||
await logo_file.save(str(save_path))
|
||||
logo_path = f"/static/uploads/logos/{filename}"
|
||||
|
||||
cover_file = files.get("cover_file")
|
||||
cover_path = supplier.get("cover_image") or ""
|
||||
if cover_file and cover_file.filename:
|
||||
upload_dir = Path(__file__).parent.parent / "static" / "uploads" / "covers"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
filename = secure_filename(f"{supplier['id']}_{cover_file.filename}")
|
||||
save_path = upload_dir / filename
|
||||
await cover_file.save(str(save_path))
|
||||
cover_path = f"/static/uploads/covers/{filename}"
|
||||
|
||||
# Multi-select categories
|
||||
categories = form.getlist("service_categories")
|
||||
categories_str = ",".join(categories)
|
||||
@@ -742,7 +752,7 @@ async def dashboard_listing_save():
|
||||
website = ?, contact_name = ?, contact_email = ?, contact_phone = ?,
|
||||
service_categories = ?, service_area = ?,
|
||||
years_in_business = ?, project_count = ?,
|
||||
logo_file = ?,
|
||||
logo_file = ?, cover_image = ?,
|
||||
services_offered = ?, contact_role = ?,
|
||||
linkedin_url = ?, instagram_url = ?, youtube_url = ?
|
||||
WHERE id = ?""",
|
||||
@@ -760,6 +770,7 @@ async def dashboard_listing_save():
|
||||
int(form.get("years_in_business", 0) or 0),
|
||||
int(form.get("project_count", 0) or 0),
|
||||
logo_path,
|
||||
cover_path,
|
||||
services_str,
|
||||
form.get("contact_role", ""),
|
||||
form.get("linkedin_url", ""),
|
||||
|
||||
@@ -100,6 +100,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lst-full">
|
||||
<label class="lst-label">Cover Photo
|
||||
<span style="font-weight:400;color:#94A3B8"> — 16:9, min 640px wide. Shown in directory search results.</span>
|
||||
</label>
|
||||
{% if supplier.cover_image %}
|
||||
<img src="{{ supplier.cover_image }}" alt="Current cover" style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:8px;margin-bottom:6px;border:1px solid #E2E8F0">
|
||||
{% endif %}
|
||||
<input type="file" name="cover_file" accept="image/*" class="lst-input" style="padding:6px 8px">
|
||||
</div>
|
||||
|
||||
<div class="lst-row">
|
||||
<div>
|
||||
<label class="lst-label">Contact Name</label>
|
||||
|
||||
1209
scratch/design_directory_cards.html
Normal file
1209
scratch/design_directory_cards.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user