Deploy ProFlo shop, admin, cashier + API (Docker Space)
Browse files- README.md +6 -6
- apps/admin-web/.env.example +1 -1
- apps/admin-web/app/admin/dashboard/page.tsx +1 -1
- apps/admin-web/app/admin/page.tsx +1 -1
- apps/admin-web/app/globals.css +1 -1
- apps/admin-web/app/layout.tsx +2 -2
- apps/admin-web/app/page.tsx +1 -1
- apps/admin-web/lib/apparel.ts +1 -1
- apps/admin-web/lib/receipt.ts +3 -3
- apps/admin-web/next-env.d.ts +1 -1
- apps/cashier-web/app/gate/page.tsx +1 -1
- apps/cashier-web/app/layout.tsx +2 -2
- apps/cashier-web/app/page.tsx +2 -2
- apps/cashier-web/next-env.d.ts +1 -1
- apps/customer-web/.env.example +2 -2
- apps/customer-web/app/globals.css +1 -1
- apps/customer-web/app/layout.tsx +1 -1
- apps/customer-web/app/page.tsx +2 -2
- apps/customer-web/app/shop/components/ShopShell.tsx +4 -4
- apps/customer-web/app/shop/context/ShopContext.tsx +3 -3
- apps/customer-web/app/shop/lib/apparel.ts +1 -1
- apps/customer-web/app/shop/lib/receiptDownload.ts +3 -3
- apps/customer-web/app/shop/page.tsx +3 -3
- apps/customer-web/lib/cartBackup.ts +1 -1
- apps/customer-web/lib/offlineCartQueue.ts +1 -1
- apps/customer-web/next-env.d.ts +1 -1
- deploy/huggingface-admin/Dockerfile +1 -1
- deploy/huggingface-admin/README.md +3 -3
- deploy/huggingface-admin/push.ps1 +6 -6
- deploy/huggingface-cashier/Dockerfile +1 -1
- deploy/huggingface-cashier/README.md +3 -3
- deploy/huggingface-cashier/push.ps1 +6 -6
- deploy/huggingface/README.md +6 -6
- deploy/huggingface/push.ps1 +5 -5
- package.json +1 -1
- packages/shared/src/apparel.ts +1 -1
- render.yaml +1 -1
- services/api/.env.example +2 -2
- services/api/src/index.ts +3 -3
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🛒
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
@@ -9,15 +9,15 @@ health_check: /health
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
#
|
| 13 |
|
| 14 |
All apps run in one Docker Space behind nginx on port **7860**.
|
| 15 |
|
| 16 |
| App | URL |
|
| 17 |
|-----|-----|
|
| 18 |
-
| Customer shop | [/shop](https://adi576-
|
| 19 |
-
| Admin HQ | [/admin](https://adi576-
|
| 20 |
-
| Cashier terminal | [/cashier](https://adi576-
|
| 21 |
| API (proxied) | `/checkout-api` |
|
| 22 |
|
| 23 |
## Required secrets
|
|
@@ -28,7 +28,7 @@ All apps run in one Docker Space behind nginx on port **7860**.
|
|
| 28 |
| `JWT_SECRET` | Long random string |
|
| 29 |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Staff login |
|
| 30 |
| `MANAGER_USERNAME` / `MANAGER_PASSWORD` | Manager login |
|
| 31 |
-
| `PUBLIC_API_URL` | `https://adi576-
|
| 32 |
|
| 33 |
Optional: `DEFAULT_STORE_CODE`, `CASHIER_API_KEY` (+ same value as `NEXT_PUBLIC_CASHIER_API_KEY` at build if you protect cashier routes).
|
| 34 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ProFlo
|
| 3 |
emoji: 🛒
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# ProFlo — Full checkout platform
|
| 13 |
|
| 14 |
All apps run in one Docker Space behind nginx on port **7860**.
|
| 15 |
|
| 16 |
| App | URL |
|
| 17 |
|-----|-----|
|
| 18 |
+
| Customer shop | [/shop](https://adi576-proflo.hf.space/shop) |
|
| 19 |
+
| Admin HQ | [/admin](https://adi576-proflo.hf.space/admin) |
|
| 20 |
+
| Cashier terminal | [/cashier](https://adi576-proflo.hf.space/cashier) |
|
| 21 |
| API (proxied) | `/checkout-api` |
|
| 22 |
|
| 23 |
## Required secrets
|
|
|
|
| 28 |
| `JWT_SECRET` | Long random string |
|
| 29 |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Staff login |
|
| 30 |
| `MANAGER_USERNAME` / `MANAGER_PASSWORD` | Manager login |
|
| 31 |
+
| `PUBLIC_API_URL` | `https://adi576-proflo.hf.space/checkout-api` |
|
| 32 |
|
| 33 |
Optional: `DEFAULT_STORE_CODE`, `CASHIER_API_KEY` (+ same value as `NEXT_PUBLIC_CASHIER_API_KEY` at build if you protect cashier routes).
|
| 34 |
|
apps/admin-web/.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000
|
| 2 |
|
| 3 |
-
# Vercel: API_PROXY_TARGET=https://
|
| 4 |
# Vercel: NEXT_PUBLIC_API_BASE_URL=/checkout-api
|
|
|
|
| 1 |
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000
|
| 2 |
|
| 3 |
+
# Vercel: API_PROXY_TARGET=https://proflo.onrender.com
|
| 4 |
# Vercel: NEXT_PUBLIC_API_BASE_URL=/checkout-api
|
apps/admin-web/app/admin/dashboard/page.tsx
CHANGED
|
@@ -1241,7 +1241,7 @@ export default function AdminDashboardPage() {
|
|
| 1241 |
|
| 1242 |
<label className="field">
|
| 1243 |
<span>Brand</span>
|
| 1244 |
-
<input value={brand} onChange={(e) => setBrand(e.target.value)} placeholder="e.g.
|
| 1245 |
</label>
|
| 1246 |
|
| 1247 |
<label className={`field${formErrors.category ? " field--invalid" : ""}`}>
|
|
|
|
| 1241 |
|
| 1242 |
<label className="field">
|
| 1243 |
<span>Brand</span>
|
| 1244 |
+
<input value={brand} onChange={(e) => setBrand(e.target.value)} placeholder="e.g. ProFlo Basics" />
|
| 1245 |
</label>
|
| 1246 |
|
| 1247 |
<label className={`field${formErrors.category ? " field--invalid" : ""}`}>
|
apps/admin-web/app/admin/page.tsx
CHANGED
|
@@ -49,7 +49,7 @@ export default function AdminLoginPage() {
|
|
| 49 |
return (
|
| 50 |
<main className="loginPage">
|
| 51 |
<div className="loginCard">
|
| 52 |
-
<h1 className="loginTitle">
|
| 53 |
<p className="loginHint">Sign in to the operations console</p>
|
| 54 |
|
| 55 |
<form onSubmit={(e) => void onSubmit(e)} className="loginForm">
|
|
|
|
| 49 |
return (
|
| 50 |
<main className="loginPage">
|
| 51 |
<div className="loginCard">
|
| 52 |
+
<h1 className="loginTitle">ProFlo HQ</h1>
|
| 53 |
<p className="loginHint">Sign in to the operations console</p>
|
| 54 |
|
| 55 |
<form onSubmit={(e) => void onSubmit(e)} className="loginForm">
|
apps/admin-web/app/globals.css
CHANGED
|
@@ -1526,7 +1526,7 @@ button.secondary {
|
|
| 1526 |
}
|
| 1527 |
|
| 1528 |
/* ==========================================================================
|
| 1529 |
-
|
| 1530 |
========================================================================== */
|
| 1531 |
|
| 1532 |
/* Layout structure */
|
|
|
|
| 1526 |
}
|
| 1527 |
|
| 1528 |
/* ==========================================================================
|
| 1529 |
+
ProFlo Operations Sidebar & Mobile Responsive Layout Redesign
|
| 1530 |
========================================================================== */
|
| 1531 |
|
| 1532 |
/* Layout structure */
|
apps/admin-web/app/layout.tsx
CHANGED
|
@@ -3,8 +3,8 @@ import type { ReactNode } from "react";
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
-
title: "
|
| 7 |
-
description: "
|
| 8 |
};
|
| 9 |
|
| 10 |
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
+
title: "ProFlo HQ",
|
| 7 |
+
description: "ProFlo operations console"
|
| 8 |
};
|
| 9 |
|
| 10 |
export default function RootLayout({ children }: { children: ReactNode }) {
|
apps/admin-web/app/page.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
| 5 |
<main className="adminHome">
|
| 6 |
-
<h1 className="adminHome__title">
|
| 7 |
<p className="adminHome__text">Operations console for catalogue, counter tokens, receipts, and store KPIs.</p>
|
| 8 |
<Link href="/admin" className="adminHome__link">
|
| 9 |
Open admin sign-in
|
|
|
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
| 5 |
<main className="adminHome">
|
| 6 |
+
<h1 className="adminHome__title">ProFlo HQ</h1>
|
| 7 |
<p className="adminHome__text">Operations console for catalogue, counter tokens, receipts, and store KPIs.</p>
|
| 8 |
<Link href="/admin" className="adminHome__link">
|
| 9 |
Open admin sign-in
|
apps/admin-web/lib/apparel.ts
CHANGED
|
@@ -56,4 +56,4 @@ export const APPAREL_GENDERS = ["Unisex", "Men", "Women", "Kids"] as const;
|
|
| 56 |
|
| 57 |
export const APPAREL_SEASONS = ["", "SS26", "AW26", "Core", "Clearance"] as const;
|
| 58 |
|
| 59 |
-
export const BRAND_NAME = "
|
|
|
|
| 56 |
|
| 57 |
export const APPAREL_SEASONS = ["", "SS26", "AW26", "Core", "Clearance"] as const;
|
| 58 |
|
| 59 |
+
export const BRAND_NAME = "ProFlo";
|
apps/admin-web/lib/receipt.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function receiptHtml(receipt: PaidReceipt): string {
|
|
| 41 |
"<!DOCTYPE html>",
|
| 42 |
'<html lang="en">',
|
| 43 |
'<head><meta charset="utf-8"/>',
|
| 44 |
-
`<title>
|
| 45 |
"<style>",
|
| 46 |
"body{font-family:system-ui,sans-serif;max-width:420px;margin:24px auto;color:#0f172a}",
|
| 47 |
"h1{font-size:1.25rem;margin:0 0 4px}",
|
|
@@ -52,7 +52,7 @@ export function receiptHtml(receipt: PaidReceipt): string {
|
|
| 52 |
".totals div{display:flex;justify-content:space-between;padding:4px 0}",
|
| 53 |
".grand{font-weight:700;font-size:1.1rem;border-top:2px solid #0f172a;padding-top:8px;margin-top:8px}",
|
| 54 |
"</style></head><body>",
|
| 55 |
-
"<h1>
|
| 56 |
`<p class="meta">Receipt ${escapeHtml(receipt.receiptNumber)} · ${escapeHtml(receipt.storeCode)}<br>${when}</p>`,
|
| 57 |
`<table><tbody>${rows}</tbody></table>`,
|
| 58 |
'<div class="totals">',
|
|
@@ -71,7 +71,7 @@ export function downloadReceiptHtml(receipt: PaidReceipt): void {
|
|
| 71 |
const url = URL.createObjectURL(blob);
|
| 72 |
const a = document.createElement("a");
|
| 73 |
a.href = url;
|
| 74 |
-
a.download = `
|
| 75 |
a.click();
|
| 76 |
URL.revokeObjectURL(url);
|
| 77 |
}
|
|
|
|
| 41 |
"<!DOCTYPE html>",
|
| 42 |
'<html lang="en">',
|
| 43 |
'<head><meta charset="utf-8"/>',
|
| 44 |
+
`<title>ProFlo receipt ${escapeHtml(receipt.receiptNumber)}</title>`,
|
| 45 |
"<style>",
|
| 46 |
"body{font-family:system-ui,sans-serif;max-width:420px;margin:24px auto;color:#0f172a}",
|
| 47 |
"h1{font-size:1.25rem;margin:0 0 4px}",
|
|
|
|
| 52 |
".totals div{display:flex;justify-content:space-between;padding:4px 0}",
|
| 53 |
".grand{font-weight:700;font-size:1.1rem;border-top:2px solid #0f172a;padding-top:8px;margin-top:8px}",
|
| 54 |
"</style></head><body>",
|
| 55 |
+
"<h1>ProFlo</h1>",
|
| 56 |
`<p class="meta">Receipt ${escapeHtml(receipt.receiptNumber)} · ${escapeHtml(receipt.storeCode)}<br>${when}</p>`,
|
| 57 |
`<table><tbody>${rows}</tbody></table>`,
|
| 58 |
'<div class="totals">',
|
|
|
|
| 71 |
const url = URL.createObjectURL(blob);
|
| 72 |
const a = document.createElement("a");
|
| 73 |
a.href = url;
|
| 74 |
+
a.download = `proflo-receipt-${receipt.receiptNumber.replace(/[^a-zA-Z0-9_-]/g, "")}.html`;
|
| 75 |
a.click();
|
| 76 |
URL.revokeObjectURL(url);
|
| 77 |
}
|
apps/admin-web/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
-
import "./.next/
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
+
import "./.next/types/routes.d.ts";
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
apps/cashier-web/app/gate/page.tsx
CHANGED
|
@@ -49,7 +49,7 @@ export default function GatePage() {
|
|
| 49 |
return (
|
| 50 |
<div className="shell">
|
| 51 |
<header className="header">
|
| 52 |
-
<p className="header__eyebrow">
|
| 53 |
<h1 className="header__title">Exit gate</h1>
|
| 54 |
<p className="header__sub">Scan the customer's exit QR after they have paid. Each code works once.</p>
|
| 55 |
<Link href="/" className="gateBack">
|
|
|
|
| 49 |
return (
|
| 50 |
<div className="shell">
|
| 51 |
<header className="header">
|
| 52 |
+
<p className="header__eyebrow">ProFlo</p>
|
| 53 |
<h1 className="header__title">Exit gate</h1>
|
| 54 |
<p className="header__sub">Scan the customer's exit QR after they have paid. Each code works once.</p>
|
| 55 |
<Link href="/" className="gateBack">
|
apps/cashier-web/app/layout.tsx
CHANGED
|
@@ -3,8 +3,8 @@ import type { ReactNode } from "react";
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
-
title: "
|
| 7 |
-
description: "
|
| 8 |
};
|
| 9 |
|
| 10 |
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
+
title: "ProFlo Counter",
|
| 7 |
+
description: "ProFlo cashier terminal"
|
| 8 |
};
|
| 9 |
|
| 10 |
export default function RootLayout({ children }: { children: ReactNode }) {
|
apps/cashier-web/app/page.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
import { useCallback, useEffect, useState } from "react";
|
| 4 |
import Link from "next/link";
|
| 5 |
import { apiBase, cashierHeaders } from "../lib/api";
|
| 6 |
-
const RECENT_KEY = "
|
| 7 |
const MAX_RECENT = 8;
|
| 8 |
|
| 9 |
type OrderLine = {
|
|
@@ -193,7 +193,7 @@ export default function CashierHomePage() {
|
|
| 193 |
return (
|
| 194 |
<div className="shell">
|
| 195 |
<header className="header">
|
| 196 |
-
<p className="header__eyebrow">
|
| 197 |
<h1 className="header__title">Counter</h1>
|
| 198 |
<p className="header__sub">
|
| 199 |
Look up queue tokens or order IDs, verify the basket, then settle cash or card at the till.
|
|
|
|
| 3 |
import { useCallback, useEffect, useState } from "react";
|
| 4 |
import Link from "next/link";
|
| 5 |
import { apiBase, cashierHeaders } from "../lib/api";
|
| 6 |
+
const RECENT_KEY = "proflo-cashier-recent";
|
| 7 |
const MAX_RECENT = 8;
|
| 8 |
|
| 9 |
type OrderLine = {
|
|
|
|
| 193 |
return (
|
| 194 |
<div className="shell">
|
| 195 |
<header className="header">
|
| 196 |
+
<p className="header__eyebrow">ProFlo</p>
|
| 197 |
<h1 className="header__title">Counter</h1>
|
| 198 |
<p className="header__sub">
|
| 199 |
Look up queue tokens or order IDs, verify the basket, then settle cash or card at the till.
|
apps/cashier-web/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
-
import "./.next/
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
+
import "./.next/types/routes.d.ts";
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
apps/customer-web/.env.example
CHANGED
|
@@ -2,8 +2,8 @@ NEXT_PUBLIC_API_BASE_URL=http://localhost:4000
|
|
| 2 |
|
| 3 |
# --- Vercel production (avoids Brave/ad-block on onrender.com) ---
|
| 4 |
# Set on Vercel (customer-web project):
|
| 5 |
-
# API_PROXY_TARGET=https://
|
| 6 |
# NEXT_PUBLIC_API_BASE_URL=/checkout-api
|
| 7 |
# On Render API, set PUBLIC_API_URL to:
|
| 8 |
-
# https://
|
| 9 |
# (so product image URLs load on the same origin)
|
|
|
|
| 2 |
|
| 3 |
# --- Vercel production (avoids Brave/ad-block on onrender.com) ---
|
| 4 |
# Set on Vercel (customer-web project):
|
| 5 |
+
# API_PROXY_TARGET=https://proflo.onrender.com
|
| 6 |
# NEXT_PUBLIC_API_BASE_URL=/checkout-api
|
| 7 |
# On Render API, set PUBLIC_API_URL to:
|
| 8 |
+
# https://proflo-customer-web.vercel.app/checkout-api
|
| 9 |
# (so product image URLs load on the same origin)
|
apps/customer-web/app/globals.css
CHANGED
|
@@ -2399,7 +2399,7 @@ a {
|
|
| 2399 |
}
|
| 2400 |
|
| 2401 |
/* ==========================================================================
|
| 2402 |
-
|
| 2403 |
========================================================================== */
|
| 2404 |
|
| 2405 |
/* Modern layout and font adjustments */
|
|
|
|
| 2399 |
}
|
| 2400 |
|
| 2401 |
/* ==========================================================================
|
| 2402 |
+
ProFlo Luxury Boutique Styling & Variant Selection Styles
|
| 2403 |
========================================================================== */
|
| 2404 |
|
| 2405 |
/* Modern layout and font adjustments */
|
apps/customer-web/app/layout.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
-
title: "
|
| 7 |
description: "Fashion & merchandising at the speed of scan."
|
| 8 |
};
|
| 9 |
|
|
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
+
title: "ProFlo",
|
| 7 |
description: "Fashion & merchandising at the speed of scan."
|
| 8 |
};
|
| 9 |
|
apps/customer-web/app/page.tsx
CHANGED
|
@@ -3,9 +3,9 @@ import Link from "next/link";
|
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
| 5 |
<main className="landing">
|
| 6 |
-
<p className="landing__eyebrow">
|
| 7 |
<h1 className="landing__title">
|
| 8 |
-
<span className="brandZipp">
|
| 9 |
</h1>
|
| 10 |
<p className="landing__text">Scan the store QR, grab what you need, and bounce — your bag stays synced while you shop.</p>
|
| 11 |
<Link href="/shop" className="btnPrimary btnPrimary--full landing__cta">
|
|
|
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
| 5 |
<main className="landing">
|
| 6 |
+
<p className="landing__eyebrow">ProFlo</p>
|
| 7 |
<h1 className="landing__title">
|
| 8 |
+
<span className="brandZipp">Scan in.</span> <span className="brandMart">Style out.</span>
|
| 9 |
</h1>
|
| 10 |
<p className="landing__text">Scan the store QR, grab what you need, and bounce — your bag stays synced while you shop.</p>
|
| 11 |
<Link href="/shop" className="btnPrimary btnPrimary--full landing__cta">
|
apps/customer-web/app/shop/components/ShopShell.tsx
CHANGED
|
@@ -41,9 +41,9 @@ export function ShopShell({ children }: { children: ReactNode }) {
|
|
| 41 |
<div className="siteHeader__inner">
|
| 42 |
{isHome ? (
|
| 43 |
<>
|
| 44 |
-
<Link href="/shop" className="siteHeader__brand" aria-label="
|
| 45 |
-
<span className="brandZipp">
|
| 46 |
-
<span className="brandMart">
|
| 47 |
</Link>
|
| 48 |
<nav className="siteHeader__nav" aria-label="Shop sections">
|
| 49 |
<Link href="/shop/scan" className="siteHeader__pill">
|
|
@@ -108,7 +108,7 @@ export function ShopShell({ children }: { children: ReactNode }) {
|
|
| 108 |
</span>
|
| 109 |
<span className="siteFooter__muted">Bag syncs while this visit is active</span>
|
| 110 |
</div>
|
| 111 |
-
<p className="siteFooter__legal">© {new Date().getFullYear()}
|
| 112 |
</div>
|
| 113 |
</footer>
|
| 114 |
|
|
|
|
| 41 |
<div className="siteHeader__inner">
|
| 42 |
{isHome ? (
|
| 43 |
<>
|
| 44 |
+
<Link href="/shop" className="siteHeader__brand" aria-label="ProFlo home">
|
| 45 |
+
<span className="brandZipp">Pro</span>
|
| 46 |
+
<span className="brandMart">Flo</span>
|
| 47 |
</Link>
|
| 48 |
<nav className="siteHeader__nav" aria-label="Shop sections">
|
| 49 |
<Link href="/shop/scan" className="siteHeader__pill">
|
|
|
|
| 108 |
</span>
|
| 109 |
<span className="siteFooter__muted">Bag syncs while this visit is active</span>
|
| 110 |
</div>
|
| 111 |
+
<p className="siteFooter__legal">© {new Date().getFullYear()} ProFlo · Fashion at the speed of scan</p>
|
| 112 |
</div>
|
| 113 |
</footer>
|
| 114 |
|
apps/customer-web/app/shop/context/ShopContext.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
| 21 |
import { apiBase, type RecommendationProduct } from "../lib/shopConfig";
|
| 22 |
|
| 23 |
export { apiBase, type RecommendationProduct } from "../lib/shopConfig";
|
| 24 |
-
const SESSION_STORAGE_KEY = "
|
| 25 |
|
| 26 |
export type CartLine = {
|
| 27 |
productId?: string;
|
|
@@ -171,7 +171,7 @@ export function ShopProvider({ children }: { children: ReactNode }) {
|
|
| 171 |
try {
|
| 172 |
const fromStorage =
|
| 173 |
typeof window !== "undefined"
|
| 174 |
-
? localStorage.getItem("
|
| 175 |
localStorage.getItem("zippmart-visit-phone")?.trim() ??
|
| 176 |
localStorage.getItem("supermart-visit-phone")?.trim() ??
|
| 177 |
""
|
|
@@ -488,7 +488,7 @@ export function ShopProvider({ children }: { children: ReactNode }) {
|
|
| 488 |
setMessage(typeof data.message === "string" ? data.message : "No visit found");
|
| 489 |
return false;
|
| 490 |
}
|
| 491 |
-
localStorage.setItem("
|
| 492 |
setSessionId(data.sessionId);
|
| 493 |
localStorage.setItem(SESSION_STORAGE_KEY, data.sessionId);
|
| 494 |
setSessionBootstrapDone(true);
|
|
|
|
| 21 |
import { apiBase, type RecommendationProduct } from "../lib/shopConfig";
|
| 22 |
|
| 23 |
export { apiBase, type RecommendationProduct } from "../lib/shopConfig";
|
| 24 |
+
const SESSION_STORAGE_KEY = "proflo-shop-session-id";
|
| 25 |
|
| 26 |
export type CartLine = {
|
| 27 |
productId?: string;
|
|
|
|
| 171 |
try {
|
| 172 |
const fromStorage =
|
| 173 |
typeof window !== "undefined"
|
| 174 |
+
? localStorage.getItem("proflo-visit-phone")?.trim() ??
|
| 175 |
localStorage.getItem("zippmart-visit-phone")?.trim() ??
|
| 176 |
localStorage.getItem("supermart-visit-phone")?.trim() ??
|
| 177 |
""
|
|
|
|
| 488 |
setMessage(typeof data.message === "string" ? data.message : "No visit found");
|
| 489 |
return false;
|
| 490 |
}
|
| 491 |
+
localStorage.setItem("proflo-visit-phone", t);
|
| 492 |
setSessionId(data.sessionId);
|
| 493 |
localStorage.setItem(SESSION_STORAGE_KEY, data.sessionId);
|
| 494 |
setSessionBootstrapDone(true);
|
apps/customer-web/app/shop/lib/apparel.ts
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
export const BRAND_NAME = "
|
|
|
|
| 1 |
+
export const BRAND_NAME = "ProFlo";
|
apps/customer-web/app/shop/lib/receiptDownload.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function receiptHtml(receipt: PaidReceipt): string {
|
|
| 33 |
"<!DOCTYPE html>",
|
| 34 |
'<html lang="en">',
|
| 35 |
'<head><meta charset="utf-8"/>',
|
| 36 |
-
`<title>
|
| 37 |
"<style>",
|
| 38 |
"body{font-family:system-ui,sans-serif;max-width:420px;margin:24px auto;color:#0f172a}",
|
| 39 |
"h1{font-size:1.25rem;margin:0 0 4px}",
|
|
@@ -44,7 +44,7 @@ export function receiptHtml(receipt: PaidReceipt): string {
|
|
| 44 |
".totals div{display:flex;justify-content:space-between;padding:4px 0}",
|
| 45 |
".grand{font-weight:700;font-size:1.1rem;border-top:2px solid #0f172a;padding-top:8px;margin-top:8px}",
|
| 46 |
"</style></head><body>",
|
| 47 |
-
"<h1>
|
| 48 |
`<p class="meta">Receipt ${escapeHtml(receipt.receiptNumber)} · ${escapeHtml(receipt.storeCode)}<br>${when}</p>`,
|
| 49 |
`<table><tbody>${rows}</tbody></table>`,
|
| 50 |
'<div class="totals">',
|
|
@@ -73,7 +73,7 @@ export function downloadReceiptHtml(receipt: PaidReceipt): void {
|
|
| 73 |
const url = URL.createObjectURL(blob);
|
| 74 |
const a = document.createElement("a");
|
| 75 |
a.href = url;
|
| 76 |
-
a.download = `
|
| 77 |
a.click();
|
| 78 |
URL.revokeObjectURL(url);
|
| 79 |
}
|
|
|
|
| 33 |
"<!DOCTYPE html>",
|
| 34 |
'<html lang="en">',
|
| 35 |
'<head><meta charset="utf-8"/>',
|
| 36 |
+
`<title>ProFlo receipt ${escapeHtml(receipt.receiptNumber)}</title>`,
|
| 37 |
"<style>",
|
| 38 |
"body{font-family:system-ui,sans-serif;max-width:420px;margin:24px auto;color:#0f172a}",
|
| 39 |
"h1{font-size:1.25rem;margin:0 0 4px}",
|
|
|
|
| 44 |
".totals div{display:flex;justify-content:space-between;padding:4px 0}",
|
| 45 |
".grand{font-weight:700;font-size:1.1rem;border-top:2px solid #0f172a;padding-top:8px;margin-top:8px}",
|
| 46 |
"</style></head><body>",
|
| 47 |
+
"<h1>ProFlo</h1>",
|
| 48 |
`<p class="meta">Receipt ${escapeHtml(receipt.receiptNumber)} · ${escapeHtml(receipt.storeCode)}<br>${when}</p>`,
|
| 49 |
`<table><tbody>${rows}</tbody></table>`,
|
| 50 |
'<div class="totals">',
|
|
|
|
| 73 |
const url = URL.createObjectURL(blob);
|
| 74 |
const a = document.createElement("a");
|
| 75 |
a.href = url;
|
| 76 |
+
a.download = `proflo-receipt-${receipt.receiptNumber.replace(/[^a-zA-Z0-9_-]/g, "")}.html`;
|
| 77 |
a.click();
|
| 78 |
URL.revokeObjectURL(url);
|
| 79 |
}
|
apps/customer-web/app/shop/page.tsx
CHANGED
|
@@ -39,7 +39,7 @@ export default function ShopHomePage() {
|
|
| 39 |
|
| 40 |
useEffect(() => {
|
| 41 |
if (typeof window === "undefined") return;
|
| 42 |
-
setVisitPhone(localStorage.getItem("
|
| 43 |
}, []);
|
| 44 |
|
| 45 |
const loadCatalog = useCallback(async () => {
|
|
@@ -112,7 +112,7 @@ export default function ShopHomePage() {
|
|
| 112 |
? new URLSearchParams(window.location.search).get("store")?.trim()?.toUpperCase()
|
| 113 |
: undefined;
|
| 114 |
if (visitPhone.trim().length >= 10) {
|
| 115 |
-
localStorage.setItem("
|
| 116 |
}
|
| 117 |
await createSession(store ?? "BLR001", visitPhone.trim().length >= 10 ? visitPhone.trim() : undefined);
|
| 118 |
}
|
|
@@ -217,7 +217,7 @@ export default function ShopHomePage() {
|
|
| 217 |
onChange={(e) => setVisitPhone(e.target.value)}
|
| 218 |
onBlur={() => {
|
| 219 |
if (visitPhone.trim().length >= 10) {
|
| 220 |
-
localStorage.setItem("
|
| 221 |
}
|
| 222 |
}}
|
| 223 |
/>
|
|
|
|
| 39 |
|
| 40 |
useEffect(() => {
|
| 41 |
if (typeof window === "undefined") return;
|
| 42 |
+
setVisitPhone(localStorage.getItem("proflo-visit-phone") ?? localStorage.getItem("zippmart-visit-phone") ?? localStorage.getItem("supermart-visit-phone") ?? "");
|
| 43 |
}, []);
|
| 44 |
|
| 45 |
const loadCatalog = useCallback(async () => {
|
|
|
|
| 112 |
? new URLSearchParams(window.location.search).get("store")?.trim()?.toUpperCase()
|
| 113 |
: undefined;
|
| 114 |
if (visitPhone.trim().length >= 10) {
|
| 115 |
+
localStorage.setItem("proflo-visit-phone", visitPhone.trim());
|
| 116 |
}
|
| 117 |
await createSession(store ?? "BLR001", visitPhone.trim().length >= 10 ? visitPhone.trim() : undefined);
|
| 118 |
}
|
|
|
|
| 217 |
onChange={(e) => setVisitPhone(e.target.value)}
|
| 218 |
onBlur={() => {
|
| 219 |
if (visitPhone.trim().length >= 10) {
|
| 220 |
+
localStorage.setItem("proflo-visit-phone", visitPhone.trim());
|
| 221 |
}
|
| 222 |
}}
|
| 223 |
/>
|
apps/customer-web/lib/cartBackup.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
const KEY = "
|
| 2 |
|
| 3 |
export type CartBackup = {
|
| 4 |
savedAt: string;
|
|
|
|
| 1 |
+
const KEY = "proflo-cart-backup-v1";
|
| 2 |
|
| 3 |
export type CartBackup = {
|
| 4 |
savedAt: string;
|
apps/customer-web/lib/offlineCartQueue.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
const QUEUE_KEY = "
|
| 2 |
const MAX = 25;
|
| 3 |
|
| 4 |
export type QueuedCartAdd = { barcode: string; qty: number; enqueuedAt: string };
|
|
|
|
| 1 |
+
const QUEUE_KEY = "proflo-offline-cart-queue";
|
| 2 |
const MAX = 25;
|
| 3 |
|
| 4 |
export type QueuedCartAdd = { barcode: string; qty: number; enqueuedAt: string };
|
apps/customer-web/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
-
import "./.next/
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
+
import "./.next/types/routes.d.ts";
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
deploy/huggingface-admin/Dockerfile
CHANGED
|
@@ -9,7 +9,7 @@ RUN npm ci
|
|
| 9 |
|
| 10 |
COPY apps/admin-web ./apps/admin-web
|
| 11 |
|
| 12 |
-
ENV NEXT_PUBLIC_API_BASE_URL=https://adi576-
|
| 13 |
RUN npm run build -w admin-web \
|
| 14 |
&& test -f apps/admin-web/.next/BUILD_ID \
|
| 15 |
&& test -d apps/admin-web/.next/standalone
|
|
|
|
| 9 |
|
| 10 |
COPY apps/admin-web ./apps/admin-web
|
| 11 |
|
| 12 |
+
ENV NEXT_PUBLIC_API_BASE_URL=https://adi576-proflo.hf.space/checkout-api
|
| 13 |
RUN npm run build -w admin-web \
|
| 14 |
&& test -f apps/admin-web/.next/BUILD_ID \
|
| 15 |
&& test -d apps/admin-web/.next/standalone
|
deploy/huggingface-admin/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 📊
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: red
|
|
@@ -8,13 +8,13 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
|
| 13 |
Staff and manager dashboard for catalogue, stock, orders, and store KPIs.
|
| 14 |
|
| 15 |
Open **/admin** to sign in.
|
| 16 |
|
| 17 |
-
Uses the API on the main
|
| 18 |
|
| 19 |
## Default logins
|
| 20 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ProFlo Admin
|
| 3 |
emoji: 📊
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: red
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# ProFlo Admin HQ
|
| 12 |
|
| 13 |
Staff and manager dashboard for catalogue, stock, orders, and store KPIs.
|
| 14 |
|
| 15 |
Open **/admin** to sign in.
|
| 16 |
|
| 17 |
+
Uses the API on the main ProFlo Space: `https://adi576-proflo.hf.space/checkout-api`
|
| 18 |
|
| 19 |
## Default logins
|
| 20 |
|
deploy/huggingface-admin/push.ps1
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
# Push admin app to Hugging Face Space ADI576/
|
| 2 |
|
| 3 |
$ErrorActionPreference = "Stop"
|
| 4 |
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
| 5 |
-
$SpaceDir = Join-Path $env:TEMP "
|
| 6 |
-
$SpaceUrl = "https://huggingface.co/spaces/ADI576/
|
| 7 |
|
| 8 |
$SkipNames = @(".git", "node_modules", ".next", "dist")
|
| 9 |
|
| 10 |
$Token = $env:HF_TOKEN
|
| 11 |
if (-not $Token) { $Token = $env:HUGGING_FACE_HUB_TOKEN }
|
| 12 |
if ($Token) {
|
| 13 |
-
$SpaceUrl = "https://user:$Token@huggingface.co/spaces/ADI576/
|
| 14 |
}
|
| 15 |
|
| 16 |
if (Test-Path $SpaceDir) { Remove-Item -Recurse -Force $SpaceDir }
|
|
@@ -44,9 +44,9 @@ Copy-Item "$Root\deploy\huggingface-admin\Dockerfile" (Join-Path $SpaceDir "Dock
|
|
| 44 |
Push-Location $SpaceDir
|
| 45 |
git add -A
|
| 46 |
if (-not (git diff --cached --quiet)) {
|
| 47 |
-
git commit -m "Deploy
|
| 48 |
}
|
| 49 |
git push
|
| 50 |
Pop-Location
|
| 51 |
|
| 52 |
-
Write-Host "Pushed. Open https://huggingface.co/spaces/ADI576/
|
|
|
|
| 1 |
+
# Push admin app to Hugging Face Space ADI576/proflo_admin
|
| 2 |
|
| 3 |
$ErrorActionPreference = "Stop"
|
| 4 |
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
| 5 |
+
$SpaceDir = Join-Path $env:TEMP "proflo-hf-admin"
|
| 6 |
+
$SpaceUrl = "https://huggingface.co/spaces/ADI576/proflo_admin"
|
| 7 |
|
| 8 |
$SkipNames = @(".git", "node_modules", ".next", "dist")
|
| 9 |
|
| 10 |
$Token = $env:HF_TOKEN
|
| 11 |
if (-not $Token) { $Token = $env:HUGGING_FACE_HUB_TOKEN }
|
| 12 |
if ($Token) {
|
| 13 |
+
$SpaceUrl = "https://user:$Token@huggingface.co/spaces/ADI576/proflo_admin"
|
| 14 |
}
|
| 15 |
|
| 16 |
if (Test-Path $SpaceDir) { Remove-Item -Recurse -Force $SpaceDir }
|
|
|
|
| 44 |
Push-Location $SpaceDir
|
| 45 |
git add -A
|
| 46 |
if (-not (git diff --cached --quiet)) {
|
| 47 |
+
git commit -m "Deploy ProFlo admin dashboard (Docker Space)"
|
| 48 |
}
|
| 49 |
git push
|
| 50 |
Pop-Location
|
| 51 |
|
| 52 |
+
Write-Host "Pushed. Open https://huggingface.co/spaces/ADI576/proflo_admin"
|
deploy/huggingface-cashier/Dockerfile
CHANGED
|
@@ -9,7 +9,7 @@ RUN npm ci
|
|
| 9 |
|
| 10 |
COPY apps/cashier-web ./apps/cashier-web
|
| 11 |
|
| 12 |
-
ENV NEXT_PUBLIC_API_BASE_URL=https://adi576-
|
| 13 |
RUN npm run build -w cashier-web \
|
| 14 |
&& test -f apps/cashier-web/.next/BUILD_ID \
|
| 15 |
&& test -d apps/cashier-web/.next/standalone
|
|
|
|
| 9 |
|
| 10 |
COPY apps/cashier-web ./apps/cashier-web
|
| 11 |
|
| 12 |
+
ENV NEXT_PUBLIC_API_BASE_URL=https://adi576-proflo.hf.space/checkout-api
|
| 13 |
RUN npm run build -w cashier-web \
|
| 14 |
&& test -f apps/cashier-web/.next/BUILD_ID \
|
| 15 |
&& test -d apps/cashier-web/.next/standalone
|
deploy/huggingface-cashier/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🧾
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
|
@@ -8,11 +8,11 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
|
| 13 |
In-store cashier terminal for order lookup and counter settlement.
|
| 14 |
|
| 15 |
-
Uses the API on the main
|
| 16 |
|
| 17 |
## Optional secrets
|
| 18 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ProFlo Cashier
|
| 3 |
emoji: 🧾
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# ProFlo Cashier Terminal
|
| 12 |
|
| 13 |
In-store cashier terminal for order lookup and counter settlement.
|
| 14 |
|
| 15 |
+
Uses the API on the main ProFlo Space: `https://adi576-proflo.hf.space/checkout-api`
|
| 16 |
|
| 17 |
## Optional secrets
|
| 18 |
|
deploy/huggingface-cashier/push.ps1
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
# Push cashier app to Hugging Face Space ADI576/
|
| 2 |
|
| 3 |
$ErrorActionPreference = "Stop"
|
| 4 |
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
| 5 |
-
$SpaceDir = Join-Path $env:TEMP "
|
| 6 |
-
$SpaceUrl = "https://huggingface.co/spaces/ADI576/
|
| 7 |
|
| 8 |
$SkipNames = @(".git", "node_modules", ".next", "dist")
|
| 9 |
|
| 10 |
$Token = $env:HF_TOKEN
|
| 11 |
if (-not $Token) { $Token = $env:HUGGING_FACE_HUB_TOKEN }
|
| 12 |
if ($Token) {
|
| 13 |
-
$SpaceUrl = "https://user:$Token@huggingface.co/spaces/ADI576/
|
| 14 |
}
|
| 15 |
|
| 16 |
if (Test-Path $SpaceDir) { Remove-Item -Recurse -Force $SpaceDir }
|
|
@@ -44,9 +44,9 @@ Copy-Item "$Root\deploy\huggingface-cashier\Dockerfile" (Join-Path $SpaceDir "Do
|
|
| 44 |
Push-Location $SpaceDir
|
| 45 |
git add -A
|
| 46 |
if (-not (git diff --cached --quiet)) {
|
| 47 |
-
git commit -m "Deploy
|
| 48 |
}
|
| 49 |
git push
|
| 50 |
Pop-Location
|
| 51 |
|
| 52 |
-
Write-Host "Pushed. Open https://huggingface.co/spaces/ADI576/
|
|
|
|
| 1 |
+
# Push cashier app to Hugging Face Space ADI576/proflo_cashier
|
| 2 |
|
| 3 |
$ErrorActionPreference = "Stop"
|
| 4 |
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
| 5 |
+
$SpaceDir = Join-Path $env:TEMP "proflo-hf-cashier"
|
| 6 |
+
$SpaceUrl = "https://huggingface.co/spaces/ADI576/proflo_cashier"
|
| 7 |
|
| 8 |
$SkipNames = @(".git", "node_modules", ".next", "dist")
|
| 9 |
|
| 10 |
$Token = $env:HF_TOKEN
|
| 11 |
if (-not $Token) { $Token = $env:HUGGING_FACE_HUB_TOKEN }
|
| 12 |
if ($Token) {
|
| 13 |
+
$SpaceUrl = "https://user:$Token@huggingface.co/spaces/ADI576/proflo_cashier"
|
| 14 |
}
|
| 15 |
|
| 16 |
if (Test-Path $SpaceDir) { Remove-Item -Recurse -Force $SpaceDir }
|
|
|
|
| 44 |
Push-Location $SpaceDir
|
| 45 |
git add -A
|
| 46 |
if (-not (git diff --cached --quiet)) {
|
| 47 |
+
git commit -m "Deploy ProFlo cashier terminal (Docker Space)"
|
| 48 |
}
|
| 49 |
git push
|
| 50 |
Pop-Location
|
| 51 |
|
| 52 |
+
Write-Host "Pushed. Open https://huggingface.co/spaces/ADI576/proflo_cashier"
|
deploy/huggingface/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🛒
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
@@ -9,15 +9,15 @@ health_check: /health
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
#
|
| 13 |
|
| 14 |
All apps run in one Docker Space behind nginx on port **7860**.
|
| 15 |
|
| 16 |
| App | URL |
|
| 17 |
|-----|-----|
|
| 18 |
-
| Customer shop | [/shop](https://adi576-
|
| 19 |
-
| Admin HQ | [/admin](https://adi576-
|
| 20 |
-
| Cashier terminal | [/cashier](https://adi576-
|
| 21 |
| API (proxied) | `/checkout-api` |
|
| 22 |
|
| 23 |
## Required secrets
|
|
@@ -28,7 +28,7 @@ All apps run in one Docker Space behind nginx on port **7860**.
|
|
| 28 |
| `JWT_SECRET` | Long random string |
|
| 29 |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Staff login |
|
| 30 |
| `MANAGER_USERNAME` / `MANAGER_PASSWORD` | Manager login |
|
| 31 |
-
| `PUBLIC_API_URL` | `https://adi576-
|
| 32 |
|
| 33 |
Optional: `DEFAULT_STORE_CODE`, `CASHIER_API_KEY` (+ same value as `NEXT_PUBLIC_CASHIER_API_KEY` at build if you protect cashier routes).
|
| 34 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ProFlo
|
| 3 |
emoji: 🛒
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# ProFlo — Full checkout platform
|
| 13 |
|
| 14 |
All apps run in one Docker Space behind nginx on port **7860**.
|
| 15 |
|
| 16 |
| App | URL |
|
| 17 |
|-----|-----|
|
| 18 |
+
| Customer shop | [/shop](https://adi576-proflo.hf.space/shop) |
|
| 19 |
+
| Admin HQ | [/admin](https://adi576-proflo.hf.space/admin) |
|
| 20 |
+
| Cashier terminal | [/cashier](https://adi576-proflo.hf.space/cashier) |
|
| 21 |
| API (proxied) | `/checkout-api` |
|
| 22 |
|
| 23 |
## Required secrets
|
|
|
|
| 28 |
| `JWT_SECRET` | Long random string |
|
| 29 |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Staff login |
|
| 30 |
| `MANAGER_USERNAME` / `MANAGER_PASSWORD` | Manager login |
|
| 31 |
+
| `PUBLIC_API_URL` | `https://adi576-proflo.hf.space/checkout-api` |
|
| 32 |
|
| 33 |
Optional: `DEFAULT_STORE_CODE`, `CASHIER_API_KEY` (+ same value as `NEXT_PUBLIC_CASHIER_API_KEY` at build if you protect cashier routes).
|
| 34 |
|
deploy/huggingface/push.ps1
CHANGED
|
@@ -3,18 +3,18 @@
|
|
| 3 |
|
| 4 |
$ErrorActionPreference = "Stop"
|
| 5 |
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
| 6 |
-
$SpaceDir = Join-Path $env:TEMP "
|
| 7 |
-
$SpaceUrl = "https://huggingface.co/spaces/ADI576/
|
| 8 |
|
| 9 |
$SkipNames = @(
|
| 10 |
".git", "node_modules", ".next", "dist",
|
| 11 |
-
"worker"
|
| 12 |
)
|
| 13 |
|
| 14 |
$Token = $env:HF_TOKEN
|
| 15 |
if (-not $Token) { $Token = $env:HUGGING_FACE_HUB_TOKEN }
|
| 16 |
if ($Token) {
|
| 17 |
-
$SpaceUrl = "https://user:$Token@huggingface.co/spaces/ADI576/
|
| 18 |
}
|
| 19 |
|
| 20 |
if (Test-Path $SpaceDir) { Remove-Item -Recurse -Force $SpaceDir }
|
|
@@ -40,7 +40,7 @@ Copy-Item "$Root\Dockerfile" (Join-Path $SpaceDir "Dockerfile") -Force
|
|
| 40 |
Push-Location $SpaceDir
|
| 41 |
git add -A
|
| 42 |
if (-not (git diff --cached --quiet)) {
|
| 43 |
-
git commit -m "Deploy
|
| 44 |
}
|
| 45 |
git push
|
| 46 |
if ($LASTEXITCODE -ne 0) {
|
|
|
|
| 3 |
|
| 4 |
$ErrorActionPreference = "Stop"
|
| 5 |
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
| 6 |
+
$SpaceDir = Join-Path $env:TEMP "proflo-hf-space"
|
| 7 |
+
$SpaceUrl = "https://huggingface.co/spaces/ADI576/ProFlo"
|
| 8 |
|
| 9 |
$SkipNames = @(
|
| 10 |
".git", "node_modules", ".next", "dist",
|
| 11 |
+
"worker", "docs", "nlp-lab", "infra", "proposals", "supabase"
|
| 12 |
)
|
| 13 |
|
| 14 |
$Token = $env:HF_TOKEN
|
| 15 |
if (-not $Token) { $Token = $env:HUGGING_FACE_HUB_TOKEN }
|
| 16 |
if ($Token) {
|
| 17 |
+
$SpaceUrl = "https://user:$Token@huggingface.co/spaces/ADI576/ProFlo"
|
| 18 |
}
|
| 19 |
|
| 20 |
if (Test-Path $SpaceDir) { Remove-Item -Recurse -Force $SpaceDir }
|
|
|
|
| 40 |
Push-Location $SpaceDir
|
| 41 |
git add -A
|
| 42 |
if (-not (git diff --cached --quiet)) {
|
| 43 |
+
git commit -m "Deploy ProFlo shop, admin, cashier + API (Docker Space)"
|
| 44 |
}
|
| 45 |
git push
|
| 46 |
if ($LASTEXITCODE -ne 0) {
|
package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
"name": "checkout-system",
|
| 3 |
"version": "1.0.0",
|
| 4 |
"private": true,
|
| 5 |
-
"description": "
|
| 6 |
"workspaces": [
|
| 7 |
"apps/*",
|
| 8 |
"services/*",
|
|
|
|
| 2 |
"name": "checkout-system",
|
| 3 |
"version": "1.0.0",
|
| 4 |
"private": true,
|
| 5 |
+
"description": "ProFlo apparel checkout platform monorepo",
|
| 6 |
"workspaces": [
|
| 7 |
"apps/*",
|
| 8 |
"services/*",
|
packages/shared/src/apparel.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const APPAREL_GENDERS = ["Unisex", "Men", "Women", "Kids"] as const;
|
|
| 56 |
|
| 57 |
export const APPAREL_SEASONS = ["", "SS26", "AW26", "Core", "Clearance"] as const;
|
| 58 |
|
| 59 |
-
export const BRAND_NAME = "
|
| 60 |
|
| 61 |
export function buildVariantSku(styleCode: string, color: string, size: string, barcode: string): string {
|
| 62 |
const parts = [styleCode, color, size]
|
|
|
|
| 56 |
|
| 57 |
export const APPAREL_SEASONS = ["", "SS26", "AW26", "Core", "Clearance"] as const;
|
| 58 |
|
| 59 |
+
export const BRAND_NAME = "ProFlo";
|
| 60 |
|
| 61 |
export function buildVariantSku(styleCode: string, color: string, size: string, barcode: string): string {
|
| 62 |
const parts = [styleCode, color, size]
|
render.yaml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
# Docs: https://render.com/docs/blueprint-spec
|
| 3 |
services:
|
| 4 |
- type: web
|
| 5 |
-
name:
|
| 6 |
runtime: node
|
| 7 |
plan: free
|
| 8 |
rootDir: services/api
|
|
|
|
| 2 |
# Docs: https://render.com/docs/blueprint-spec
|
| 3 |
services:
|
| 4 |
- type: web
|
| 5 |
+
name: proflo-api
|
| 6 |
runtime: node
|
| 7 |
plan: free
|
| 8 |
rootDir: services/api
|
services/api/.env.example
CHANGED
|
@@ -37,9 +37,9 @@ DEFAULT_STORE_CODE=BLR001
|
|
| 37 |
# 1. Sign up at resend.com (free tier: ~100 emails/day)
|
| 38 |
# 2. Add and verify your domain, or use onboarding address for testing only
|
| 39 |
# 3. Create an API key → set RESEND_API_KEY
|
| 40 |
-
# 4. Set EMAIL_FROM to a verified sender, e.g.
|
| 41 |
# RESEND_API_KEY=re_xxxxxxxx
|
| 42 |
-
# EMAIL_FROM=
|
| 43 |
|
| 44 |
# Optional — generic webhook if you use Make/Zapier instead of Resend
|
| 45 |
# RECEIPT_NOTIFY_WEBHOOK_URL=https://hooks.example.com/receipt
|
|
|
|
| 37 |
# 1. Sign up at resend.com (free tier: ~100 emails/day)
|
| 38 |
# 2. Add and verify your domain, or use onboarding address for testing only
|
| 39 |
# 3. Create an API key → set RESEND_API_KEY
|
| 40 |
+
# 4. Set EMAIL_FROM to a verified sender, e.g. ProFlo <receipts@yourdomain.com>
|
| 41 |
# RESEND_API_KEY=re_xxxxxxxx
|
| 42 |
+
# EMAIL_FROM=ProFlo <onboarding@resend.dev>
|
| 43 |
|
| 44 |
# Optional — generic webhook if you use Make/Zapier instead of Resend
|
| 45 |
# RECEIPT_NOTIFY_WEBHOOK_URL=https://hooks.example.com/receipt
|
services/api/src/index.ts
CHANGED
|
@@ -287,11 +287,11 @@ function receiptEmailHtml(order: Order, receiptNumber: string): string {
|
|
| 287 |
)
|
| 288 |
.join("");
|
| 289 |
return `<!DOCTYPE html><html><body style="font-family:sans-serif;color:#111">
|
| 290 |
-
<h2>
|
| 291 |
<p><strong>${receiptNumber}</strong> · ${order.storeCode}</p>
|
| 292 |
<table cellpadding="6" cellspacing="0" border="0">${rows}</table>
|
| 293 |
<p>Subtotal: ₹${order.subtotal.toFixed(2)}<br>Tax: ₹${order.taxTotal.toFixed(2)}<br><strong>Total: ₹${order.total.toFixed(2)}</strong></p>
|
| 294 |
-
<p>Thank you for shopping with
|
| 295 |
</body></html>`;
|
| 296 |
}
|
| 297 |
|
|
@@ -308,7 +308,7 @@ async function sendReceiptEmail(order: Order, receiptNumber: string): Promise<bo
|
|
| 308 |
body: JSON.stringify({
|
| 309 |
from: emailFrom,
|
| 310 |
to: [to],
|
| 311 |
-
subject: `Your
|
| 312 |
html: receiptEmailHtml(order, receiptNumber)
|
| 313 |
})
|
| 314 |
});
|
|
|
|
| 287 |
)
|
| 288 |
.join("");
|
| 289 |
return `<!DOCTYPE html><html><body style="font-family:sans-serif;color:#111">
|
| 290 |
+
<h2>ProFlo receipt</h2>
|
| 291 |
<p><strong>${receiptNumber}</strong> · ${order.storeCode}</p>
|
| 292 |
<table cellpadding="6" cellspacing="0" border="0">${rows}</table>
|
| 293 |
<p>Subtotal: ₹${order.subtotal.toFixed(2)}<br>Tax: ₹${order.taxTotal.toFixed(2)}<br><strong>Total: ₹${order.total.toFixed(2)}</strong></p>
|
| 294 |
+
<p>Thank you for shopping with ProFlo.</p>
|
| 295 |
</body></html>`;
|
| 296 |
}
|
| 297 |
|
|
|
|
| 308 |
body: JSON.stringify({
|
| 309 |
from: emailFrom,
|
| 310 |
to: [to],
|
| 311 |
+
subject: `Your ProFlo receipt ${receiptNumber}`,
|
| 312 |
html: receiptEmailHtml(order, receiptNumber)
|
| 313 |
})
|
| 314 |
});
|