feat(zoho): WebToLead forwarding mode, veteran-owned certified badge, Docker/env/CI updates (batch 0.9.1)
This commit is contained in:
parent
05b27d216a
commit
d074e597b2
13
.env.example
13
.env.example
|
|
@ -5,8 +5,17 @@ NODE_ENV=production
|
|||
SERVER_PORT=3001
|
||||
|
||||
# Zoho CRM Integration
|
||||
# Set ZOHO_ENABLED=true to forward leads/support to Zoho CRM
|
||||
# Get credentials from https://api-console.zoho.com → Self Client
|
||||
# Preferred current setup: webtolead for contact leads using the legacy Zoho form tokens.
|
||||
ZOHO_FORWARDING_MODE=webtolead
|
||||
ZOHO_WEBTOLEAD_ENABLED=false
|
||||
ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
||||
ZOHO_WEBTOLEAD_XNQSJSDP=
|
||||
ZOHO_WEBTOLEAD_XMIWTLD=
|
||||
ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
||||
ZOHO_WEBTOLEAD_RETURN_URL=null
|
||||
ZOHO_WEBTOLEAD_ZC_GAD=
|
||||
|
||||
# Standby REST API/OAuth setup. Set ZOHO_FORWARDING_MODE=api and ZOHO_ENABLED=true to use it.
|
||||
ZOHO_ENABLED=false
|
||||
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ FUTURE.md
|
|||
HISTORY.md
|
||||
BUILD_SUMMARY.md
|
||||
SCRIPTS.md
|
||||
.drop/
|
||||
zoho.md
|
||||
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
|
@ -36,3 +39,4 @@ pnpm-debug.log*
|
|||
.learnings/
|
||||
Levi.md
|
||||
Queue-North-Website.code-workspace
|
||||
Working Site.zip
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ ENV SERVER_PORT=3001
|
|||
ENV RATE_LIMIT_PER_MINUTE=5
|
||||
ENV CORS_ORIGIN=*
|
||||
ENV LOG_LEVEL=info
|
||||
ENV ZOHO_FORWARDING_MODE=webtolead
|
||||
ENV ZOHO_WEBTOLEAD_ENABLED=false
|
||||
ENV ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
||||
ENV ZOHO_WEBTOLEAD_XNQSJSDP=
|
||||
ENV ZOHO_WEBTOLEAD_XMIWTLD=
|
||||
ENV ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
||||
ENV ZOHO_WEBTOLEAD_RETURN_URL=null
|
||||
ENV ZOHO_WEBTOLEAD_ZC_GAD=
|
||||
ENV ZOHO_ENABLED=false
|
||||
ENV ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||
ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
|
|
@ -22,12 +22,21 @@ services:
|
|||
- RATE_LIMIT_PER_MINUTE=5
|
||||
- CORS_ORIGIN=https://queuenorth.com
|
||||
- LOG_LEVEL=info
|
||||
- ZOHO_ENABLED=false
|
||||
- ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||
- ZOHO_CLIENT_ID=
|
||||
- ZOHO_CLIENT_SECRET=
|
||||
- ZOHO_REFRESH_TOKEN=
|
||||
- ZOHO_REDIRECT_URI=
|
||||
- ZOHO_FORWARDING_MODE=${ZOHO_FORWARDING_MODE:-webtolead}
|
||||
- ZOHO_WEBTOLEAD_ENABLED=${ZOHO_WEBTOLEAD_ENABLED:-false}
|
||||
- ZOHO_WEBTOLEAD_URL=${ZOHO_WEBTOLEAD_URL:-https://crm.zoho.com/crm/WebToLeadForm}
|
||||
- ZOHO_WEBTOLEAD_XNQSJSDP=${ZOHO_WEBTOLEAD_XNQSJSDP:-}
|
||||
- ZOHO_WEBTOLEAD_XMIWTLD=${ZOHO_WEBTOLEAD_XMIWTLD:-}
|
||||
- ZOHO_WEBTOLEAD_ACTION_TYPE=${ZOHO_WEBTOLEAD_ACTION_TYPE:-TGVhZHM=}
|
||||
- ZOHO_WEBTOLEAD_RETURN_URL=${ZOHO_WEBTOLEAD_RETURN_URL:-null}
|
||||
- ZOHO_WEBTOLEAD_ZC_GAD=${ZOHO_WEBTOLEAD_ZC_GAD:-}
|
||||
- ZOHO_ENABLED=${ZOHO_ENABLED:-false}
|
||||
- ZOHO_API_DOMAIN=${ZOHO_API_DOMAIN:-https://www.zohoapis.com}
|
||||
- ZOHO_ACCOUNTS_DOMAIN=${ZOHO_ACCOUNTS_DOMAIN:-https://accounts.zoho.com}
|
||||
- ZOHO_CLIENT_ID=${ZOHO_CLIENT_ID:-}
|
||||
- ZOHO_CLIENT_SECRET=${ZOHO_CLIENT_SECRET:-}
|
||||
- ZOHO_REFRESH_TOKEN=${ZOHO_REFRESH_TOKEN:-}
|
||||
- ZOHO_CASES_ENABLED=${ZOHO_CASES_ENABLED:-false}
|
||||
- RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED:-false}
|
||||
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY:-}
|
||||
- RECAPTCHA_MIN_SCORE=${RECAPTCHA_MIN_SCORE:-0.5}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,36 @@
|
|||
# Zoho CRM Setup Guide for Queue North Admins
|
||||
|
||||
This guide walks you through setting up Zoho CRM integration to automatically capture leads and support cases from Queue North's website.
|
||||
This guide walks you through the current Zoho CRM integration. Contact leads use the legacy Zoho WebToLead form tokens, while the OAuth/API integration remains available as a standby option for future lead upserts or support cases.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
- A Zoho CRM account (admin access required)
|
||||
- Access to Zoho API Console: https://api-console.zoho.com
|
||||
- The WebToLead hidden field values from the old Zoho form
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create a Zoho Self-Client App
|
||||
## Step 1: Gather WebToLead Values
|
||||
|
||||
The current integration needs the old Zoho form's hidden fields:
|
||||
|
||||
```text
|
||||
xnQsjsdp
|
||||
xmIwtLD
|
||||
actionType
|
||||
returnURL
|
||||
zc_gad
|
||||
```
|
||||
|
||||
These values are stored locally in `zoho.md`, which is ignored by git.
|
||||
|
||||
---
|
||||
|
||||
## Optional Standby: Create a Zoho Self-Client App
|
||||
|
||||
Only use these OAuth steps if switching `ZOHO_FORWARDING_MODE=api`.
|
||||
|
||||
1. Go to **https://api-console.zoho.com**
|
||||
2. Click **"Create Self Client"**
|
||||
|
|
@ -29,7 +47,7 @@ Before you begin, ensure you have:
|
|||
|
||||
---
|
||||
|
||||
## Step 2: Generate an Authorization Code
|
||||
## Optional Standby: Generate an Authorization Code
|
||||
|
||||
1. In the Self Client tab, click **"Generate Code"**
|
||||
2. Set the **Scope** to:
|
||||
|
|
@ -42,7 +60,7 @@ Before you begin, ensure you have:
|
|||
|
||||
---
|
||||
|
||||
## Step 3: Exchange Auth Code for Tokens
|
||||
## Optional Standby: Exchange Auth Code for Tokens
|
||||
|
||||
Run this `curl` command (replace placeholders):
|
||||
|
||||
|
|
@ -69,12 +87,27 @@ curl -X POST https://accounts.zoho.com/oauth/v2/token \
|
|||
|
||||
---
|
||||
|
||||
## Step 4: Configure Environment Variables
|
||||
## Step 2: Configure Environment Variables
|
||||
|
||||
Add these to your `.env` file:
|
||||
The current production-friendly setup uses the legacy Zoho WebToLead form tokens for contact leads while keeping the OAuth API integration available as a standby option.
|
||||
|
||||
Add these to your `.env` file for WebToLead lead forwarding:
|
||||
|
||||
```env
|
||||
# Zoho integration is OFF by default — set to true to enable
|
||||
ZOHO_FORWARDING_MODE=webtolead
|
||||
ZOHO_WEBTOLEAD_ENABLED=true
|
||||
ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
||||
ZOHO_WEBTOLEAD_XNQSJSDP=<from Zoho WebToLead hidden field>
|
||||
ZOHO_WEBTOLEAD_XMIWTLD=<from Zoho WebToLead hidden field>
|
||||
ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
||||
ZOHO_WEBTOLEAD_RETURN_URL=null
|
||||
ZOHO_WEBTOLEAD_ZC_GAD=
|
||||
```
|
||||
|
||||
Use these only if switching back to the Zoho CRM REST API/OAuth integration:
|
||||
|
||||
```env
|
||||
ZOHO_FORWARDING_MODE=api
|
||||
ZOHO_ENABLED=false
|
||||
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||
|
|
@ -85,7 +118,7 @@ ZOHO_REFRESH_TOKEN=<from Step 3>
|
|||
ZOHO_CASES_ENABLED=false
|
||||
```
|
||||
|
||||
> **Note:** Both `ZOHO_ENABLED` and `ZOHO_CASES_ENABLED` default to `false`. Set them to `true` only after completing Steps 1–3 and verifying your credentials.
|
||||
> **Note:** `ZOHO_CASES_ENABLED` only applies to the OAuth/API path. The WebToLead values found in the old site are for lead capture only.
|
||||
|
||||
### Datacenter Variants
|
||||
|
||||
|
|
@ -100,7 +133,7 @@ If your Zoho datacenter is **outside the US**, adjust the domains:
|
|||
|
||||
---
|
||||
|
||||
## Step 5: Test the Integration
|
||||
## Step 3: Test the Integration
|
||||
|
||||
### Test Lead Capture
|
||||
1. Submit a lead on the contact form (name, email, phone, message)
|
||||
|
|
@ -156,7 +189,7 @@ Website Contact Form → SQLite (always saved)
|
|||
3. `refresh_token` never expires — store it securely
|
||||
|
||||
### Upsert Logic
|
||||
- **Leads**: Email-based upsert (update if exists, create if new)
|
||||
- **Leads**: WebToLead creates leads through the legacy Zoho form endpoint. API mode uses email-based upsert.
|
||||
- **Cases**: Always insert (new case per submission)
|
||||
|
||||
### Fire-and-Forget Design
|
||||
|
|
@ -171,7 +204,7 @@ Website Contact Form → SQLite (always saved)
|
|||
|
||||
After configuration:
|
||||
1. Deploy the environment variables to production
|
||||
2. Set `ZOHO_ENABLED=true` and `ZOHO_CASES_ENABLED=true` in production `.env`
|
||||
2. Set `ZOHO_WEBTOLEAD_ENABLED=true` in production `.env`
|
||||
3. Restart the application
|
||||
4. Submit a test lead and support case to verify data flows to Zoho CRM
|
||||
5. Check Zoho CRM Leads and Cases tabs to confirm both appear
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -374,6 +374,14 @@ const ZOHO_ACCOUNTS_DOMAIN = process.env.ZOHO_ACCOUNTS_DOMAIN || 'https://accoun
|
|||
const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null
|
||||
const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null
|
||||
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null
|
||||
const ZOHO_FORWARDING_MODE = (process.env.ZOHO_FORWARDING_MODE || 'api').toLowerCase()
|
||||
const ZOHO_WEBTOLEAD_ENABLED = process.env.ZOHO_WEBTOLEAD_ENABLED === 'true'
|
||||
const ZOHO_WEBTOLEAD_URL = process.env.ZOHO_WEBTOLEAD_URL || 'https://crm.zoho.com/crm/WebToLeadForm'
|
||||
const ZOHO_WEBTOLEAD_XNQSJSDP = process.env.ZOHO_WEBTOLEAD_XNQSJSDP || null
|
||||
const ZOHO_WEBTOLEAD_XMIWTLD = process.env.ZOHO_WEBTOLEAD_XMIWTLD || null
|
||||
const ZOHO_WEBTOLEAD_ACTION_TYPE = process.env.ZOHO_WEBTOLEAD_ACTION_TYPE || 'TGVhZHM='
|
||||
const ZOHO_WEBTOLEAD_RETURN_URL = process.env.ZOHO_WEBTOLEAD_RETURN_URL || 'null'
|
||||
const ZOHO_WEBTOLEAD_ZC_GAD = process.env.ZOHO_WEBTOLEAD_ZC_GAD || ''
|
||||
|
||||
// In-memory access token cache
|
||||
let zohoAccessToken = null
|
||||
|
|
@ -536,6 +544,70 @@ async function forwardToZoho(leadData) {
|
|||
}
|
||||
}
|
||||
|
||||
async function forwardToZohoWebToLead(leadData) {
|
||||
if (!ZOHO_WEBTOLEAD_ENABLED) return
|
||||
|
||||
if (!ZOHO_WEBTOLEAD_XNQSJSDP || !ZOHO_WEBTOLEAD_XMIWTLD) {
|
||||
log.warn('[Zoho WebToLead] Skipping forwarding - hidden form tokens are not configured')
|
||||
return
|
||||
}
|
||||
|
||||
const descriptionParts = []
|
||||
if (leadData.message) descriptionParts.push(leadData.message)
|
||||
if (leadData.service_interest) descriptionParts.push(`Service Interest: ${leadData.service_interest}`)
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
xnQsjsdp: ZOHO_WEBTOLEAD_XNQSJSDP,
|
||||
zc_gad: ZOHO_WEBTOLEAD_ZC_GAD,
|
||||
xmIwtLD: ZOHO_WEBTOLEAD_XMIWTLD,
|
||||
actionType: ZOHO_WEBTOLEAD_ACTION_TYPE,
|
||||
returnURL: ZOHO_WEBTOLEAD_RETURN_URL,
|
||||
aG9uZXlwb3Q: '',
|
||||
Company: leadData.company || '',
|
||||
'Last Name': leadData.name || 'Unknown',
|
||||
Email: leadData.email || '',
|
||||
Phone: leadData.phone || '',
|
||||
'Zip Code': leadData.zip || '',
|
||||
Description: descriptionParts.join('\n\n'),
|
||||
})
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), ZOHO_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const response = await fetch(ZOHO_WEBTOLEAD_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: payload.toString(),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
log.error(`[Zoho WebToLead] Forwarding failed (${response.status}):`, text.slice(0, 500))
|
||||
return
|
||||
}
|
||||
|
||||
log.info('[Zoho WebToLead] Lead forwarded successfully')
|
||||
} catch (fetchErr) {
|
||||
if (fetchErr.name === 'AbortError') {
|
||||
log.warn('[Zoho WebToLead] Lead forwarding timed out after', ZOHO_TIMEOUT_MS, 'ms')
|
||||
} else {
|
||||
log.error('[Zoho WebToLead] Forwarding error:', fetchErr.message)
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
function forwardLeadToZoho(leadData) {
|
||||
if (ZOHO_FORWARDING_MODE === 'webtolead') {
|
||||
return forwardToZohoWebToLead(leadData)
|
||||
}
|
||||
|
||||
return forwardToZoho(leadData)
|
||||
}
|
||||
|
||||
// --- Zoho Cases Forwarding (best-effort, fire-and-forget) ---
|
||||
async function forwardSupportToZoho(supportData) {
|
||||
if (!ZOHO_CASES_ENABLED) return
|
||||
|
|
@ -703,7 +775,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), async (req, res) => {
|
|||
log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`)
|
||||
|
||||
// Fire-and-forget Zoho forwarding (best-effort, non-blocking)
|
||||
forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
||||
forwardLeadToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
||||
|
||||
res.json({ success: true, message: "Thanks! We'll be in touch shortly." })
|
||||
} catch (err) {
|
||||
|
|
@ -713,7 +785,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), async (req, res) => {
|
|||
log.warn(`Duplicate lead email: ${sanitized.email}`)
|
||||
|
||||
// Still forward to Zoho (non-blocking) for existing leads
|
||||
forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
||||
forwardLeadToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
||||
|
||||
return res.status(409).json({
|
||||
error: 'Duplicate lead',
|
||||
|
|
@ -857,13 +929,16 @@ app.get('*', (req, res, next) => {
|
|||
app.listen(PORT, () => {
|
||||
log.info(`Server running on http://localhost:${PORT}`)
|
||||
log.info(`Health check: http://localhost:${PORT}/api/health`)
|
||||
if (ZOHO_ENABLED) {
|
||||
log.info(`Zoho CRM forwarding: ENABLED`)
|
||||
log.info(`Zoho lead forwarding mode: ${ZOHO_FORWARDING_MODE}`)
|
||||
if (ZOHO_FORWARDING_MODE === 'webtolead') {
|
||||
log.info(`Zoho WebToLead forwarding: ${ZOHO_WEBTOLEAD_ENABLED ? 'ENABLED' : 'DISABLED'}`)
|
||||
} else if (ZOHO_ENABLED) {
|
||||
log.info(`Zoho CRM API forwarding: ENABLED`)
|
||||
log.info(`Zoho API domain: ${ZOHO_API_DOMAIN}`)
|
||||
log.info(`Zoho Accounts domain: ${ZOHO_ACCOUNTS_DOMAIN}`)
|
||||
log.info(`Zoho Cases forwarding: ${process.env.ZOHO_CASES_ENABLED === 'true' ? 'ENABLED' : 'DISABLED'}`)
|
||||
} else {
|
||||
log.info('Zoho CRM forwarding: DISABLED (set ZOHO_ENABLED=true to enable)')
|
||||
log.info('Zoho CRM API forwarding: DISABLED (set ZOHO_ENABLED=true to enable)')
|
||||
}
|
||||
log.info(`Rate limiting: ${rateLimitMax} requests per ${rateLimitWindowMs / 1000} seconds`)
|
||||
log.info(`Security headers: Helmet enabled with CSP configured`)
|
||||
|
|
|
|||
|
|
@ -141,6 +141,13 @@ const Footer = () => {
|
|||
{/* Veteran Owned & Operated */}
|
||||
<div className="border-t border-white/10 mt-10 py-6">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<span className="flex h-24 w-20 items-center justify-center rounded-md border border-white/10 bg-white p-1 shadow-sm">
|
||||
<img
|
||||
src="/assets/brand/veteran-owned-certified.webp"
|
||||
alt="SBA Veteran-Owned Certified badge"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</span>
|
||||
<p className="text-primary-cyan text-xs font-semibold uppercase tracking-[0.18em]">
|
||||
Veteran Owned & Operated
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import SEO from '@/components/SEO'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowRight, Award, CheckCircle2, Compass, Cpu, Handshake, Headphones, Route, Users, Wrench } from 'lucide-react'
|
||||
import { ArrowRight, Award, CheckCircle2, Compass, Cpu, Handshake, Headphones, Route, Wrench } from 'lucide-react'
|
||||
|
||||
const proofPoints = [
|
||||
{ label: '25+ years', detail: 'Communications and infrastructure experience', icon: Award, containerClass: '' },
|
||||
|
|
@ -20,7 +20,14 @@ const proofPoints = [
|
|||
logoClassName: 'h-full w-full scale-[2]',
|
||||
containerClass: 'p-1 overflow-hidden',
|
||||
},
|
||||
{ label: 'Veteran owned', detail: 'Disciplined delivery and direct accountability', icon: Users, containerClass: '' },
|
||||
{
|
||||
label: 'Veteran-Owned Certified',
|
||||
detail: 'Disciplined delivery and direct accountability',
|
||||
logo: '/assets/brand/veteran-owned-certified.webp',
|
||||
logoAlt: 'SBA Veteran-Owned Certified badge',
|
||||
logoClassName: 'h-full w-full',
|
||||
containerClass: 'p-1',
|
||||
},
|
||||
]
|
||||
|
||||
const operatingPrinciples = [
|
||||
|
|
|
|||
|
|
@ -190,10 +190,14 @@ const Home = () => {
|
|||
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Cisco Certified Partner</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
|
||||
<span className="flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white">
|
||||
<ShieldCheck className="h-8 w-8 text-primary-blue" aria-hidden="true" />
|
||||
<span className="flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white p-1">
|
||||
<img
|
||||
src="/assets/brand/veteran-owned-certified.webp"
|
||||
alt="SBA Veteran-Owned Certified badge"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</span>
|
||||
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Veteran Owned</span>
|
||||
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Veteran-Owned Certified</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
|
||||
<span className="font-numeric flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white text-2xl font-semibold text-primary-navy">
|
||||
|
|
|
|||
Loading…
Reference in New Issue