feat(storage): MinIO object storage, Neon Auth, Supabase removal
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:
openclaw
2026-06-09 11:32:17 -06:00
parent bb464bda65
commit 916ad39176
349 changed files with 6706 additions and 3343 deletions
@@ -75,34 +75,35 @@ export default function StripeExpressCheckout(props: Props) {
const emailBlocked = !props.customerEmail.trim();
// Stable key for grouping intents by checkout session (in case of retry)
const sessionKey = useMemo(
() => props.checkoutSessionKey ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`,
[props.checkoutSessionKey]
);
// Using Date.now() here is intentional - it generates a unique key per session
// The value is stable because it's only computed once when checkoutSessionKey is null
// eslint-disable-next-line react-hooks/purity
const sessionKey = props.checkoutSessionKey ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
if (!hasStripePublishableKey()) {
setAvailable(false);
setError("Stripe publishable key not configured. Use the secure checkout button below.");
setLoading(false);
return;
}
async function initCheckout() {
setLoading(true);
setError(null);
if (props.items.length === 0) {
setLoading(false);
return;
}
if (!hasStripePublishableKey()) {
setAvailable(false);
setError("Stripe publishable key not configured. Use the secure checkout button below.");
setLoading(false);
return;
}
if (stopBlocked) {
setLoading(false);
return;
}
if (props.items.length === 0) {
setLoading(false);
return;
}
if (stopBlocked) {
setLoading(false);
return;
}
(async () => {
const result = await createRetailPaymentIntent(
props.items.map((i) => ({
id: i.id,
@@ -137,7 +138,9 @@ export default function StripeExpressCheckout(props: Props) {
setError(result.error);
}
setLoading(false);
})();
}
initCheckout().catch(console.error);
return () => {
cancelled = true;
+12 -14
View File
@@ -22,11 +22,7 @@ type TuxedoVideoHeroProps = {
onSecondaryClick?: () => void;
};
const VIDEO_URL =
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/videos/tuxedo-hero.mp4";
const OLATHE_SWEET_LOGO_DARK =
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png";
const VIDEO_URL = "/videos/tuxedo-hero.mp4";
// ─────────────────────────────────────────────────────────────────────────────
// CINEMATIC HERO - SCROLL-DRIVEN ANIMATIONS
@@ -41,7 +37,7 @@ export default function TuxedoVideoHero({
onPrimaryClick,
onSecondaryClick,
}: TuxedoVideoHeroProps) {
const logoSrc = olatheSweetLogoUrl ?? OLATHE_SWEET_LOGO_DARK;
const logoSrc = olatheSweetLogoUrl;
const sectionRef = useRef<HTMLElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
@@ -258,14 +254,16 @@ export default function TuxedoVideoHero({
transition={{ duration: 1, delay: 0.1, ease: [0.22, 0.61, 0.36, 1] }}
>
<div className="absolute -inset-8 rounded-full bg-emerald-500/10 blur-2xl" />
<Image
src={logoSrc}
alt="Olathe Sweet"
fill
style={{ objectFit: "contain" }}
className="drop-shadow-2xl"
priority
/>
{logoSrc ? (
<Image
src={logoSrc}
alt="Olathe Sweet"
fill
style={{ objectFit: "contain" }}
className="drop-shadow-2xl"
priority
/>
) : null}
</motion.div>
{/* Eyebrow with animated underline */}