feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
This commit is contained in:
@@ -41,6 +41,15 @@ import { PageHeader, AdminButton, AdminSearchInput, AdminFilterTabs, AdminBadge
|
||||
|
||||
type Tab = "dashboard" | "customers" | "products" | "orders" | "settings";
|
||||
|
||||
// SVG Icon for Wholesale - defined at module level to prevent recreation on each render
|
||||
function WholesaleIcon() {
|
||||
return (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WholesaleClient({ brandId }: { brandId: string }) {
|
||||
const [tab, setTab] = useState<Tab>("dashboard");
|
||||
const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null);
|
||||
@@ -101,13 +110,6 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// SVG Icon for Wholesale
|
||||
const WholesaleIcon = () => (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{ value: "dashboard", label: "Dashboard", count: undefined },
|
||||
{ value: "customers", label: "Customers", count: customers.filter(c => c.account_status !== "pending_approval" && c.account_status !== "rejected").length },
|
||||
@@ -116,10 +118,13 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
|
||||
{ value: "settings", label: "Settings" },
|
||||
];
|
||||
|
||||
// Declare icon reference for PageHeader
|
||||
const pageIcon = <WholesaleIcon />;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<PageHeader
|
||||
icon={<WholesaleIcon />}
|
||||
icon={pageIcon}
|
||||
title="Wholesale Portal"
|
||||
subtitle="Manage wholesale orders, customers, and products"
|
||||
className="mb-0"
|
||||
@@ -1843,13 +1848,7 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
|
||||
onRefresh: () => void;
|
||||
canManageSettings: boolean;
|
||||
}) {
|
||||
if (!canManageSettings) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-slate-400">You do not have permission to manage settings.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Hooks must be called unconditionally - always declare them first
|
||||
const [form, setForm] = useState({
|
||||
requireApproval: settings?.require_approval ?? true,
|
||||
minOrderAmount: settings?.min_order_amount ?? "",
|
||||
@@ -1868,6 +1867,15 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Permission check after hooks
|
||||
if (!canManageSettings) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-slate-400">You do not have permission to manage settings.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
const result = await saveWholesaleSettings({
|
||||
|
||||
Reference in New Issue
Block a user