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:
@@ -21,9 +21,6 @@ import {
|
||||
const BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||
const BRAND_NAME = "Tuxedo Corn";
|
||||
|
||||
const OLATHE_SWEET_LOGO_DARK =
|
||||
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png";
|
||||
|
||||
// ── Translations ───────────────────────────────────────────────────────────────
|
||||
|
||||
type Translations = {
|
||||
@@ -167,10 +164,13 @@ interface OpenEntry {
|
||||
export default function TimeTrackingFieldClient({
|
||||
brandId,
|
||||
brandName,
|
||||
brandAccent,
|
||||
logoUrl,
|
||||
}: {
|
||||
brandId: string;
|
||||
brandName: string;
|
||||
brandAccent: string;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
const [lang, setLang] = useState<"en" | "es">("en");
|
||||
const [screen, setScreen] = useState<Screen>("pin");
|
||||
@@ -385,7 +385,7 @@ export default function TimeTrackingFieldClient({
|
||||
<div className="bg-zinc-900 border-b border-zinc-800 px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="relative inline-block h-8 w-auto">
|
||||
<Image src={OLATHE_SWEET_LOGO_DARK} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="opacity-80" />
|
||||
{logoUrl ? <Image src={logoUrl} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="opacity-80" /> : null}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-zinc-400">{brandName}</span>
|
||||
</div>
|
||||
@@ -410,6 +410,7 @@ export default function TimeTrackingFieldClient({
|
||||
onSubmit={handlePinSubmit}
|
||||
error={error}
|
||||
submitting={submitting}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -421,6 +422,7 @@ export default function TimeTrackingFieldClient({
|
||||
onSelectTask={handleSelectTask}
|
||||
error={error}
|
||||
submitting={submitting}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -433,6 +435,7 @@ export default function TimeTrackingFieldClient({
|
||||
showOvertimeWarning={showOvertimeWarning ?? false}
|
||||
onClockOut={() => setScreen("clock_out_form")}
|
||||
onLogout={handleLogout}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -452,6 +455,7 @@ export default function TimeTrackingFieldClient({
|
||||
onBack={() => setScreen("working")}
|
||||
error={error}
|
||||
submitting={submitting}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -462,6 +466,7 @@ export default function TimeTrackingFieldClient({
|
||||
payPeriod={payPeriod}
|
||||
clockOutResult={clockOutResult}
|
||||
onBackToPin={handleBackToPin}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -483,6 +488,7 @@ function PayPeriodCard({
|
||||
lang: "en" | "es";
|
||||
payPeriod: PayPeriodHours | null;
|
||||
showWarning: boolean;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
if (!payPeriod) return null;
|
||||
|
||||
@@ -577,6 +583,7 @@ function WorkingScreen({
|
||||
showOvertimeWarning,
|
||||
onClockOut,
|
||||
onLogout,
|
||||
logoUrl,
|
||||
}: {
|
||||
t: Translations;
|
||||
lang: "en" | "es";
|
||||
@@ -585,10 +592,11 @@ function WorkingScreen({
|
||||
showOvertimeWarning: boolean;
|
||||
onClockOut: () => void;
|
||||
onLogout: () => void;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PayPeriodCard t={t} lang={lang} payPeriod={payPeriod} showWarning={showOvertimeWarning} />
|
||||
<PayPeriodCard t={t} lang={lang} payPeriod={payPeriod} showWarning={showOvertimeWarning} logoUrl={logoUrl} />
|
||||
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-5 text-center">
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 rounded-full bg-emerald-900/40 border border-emerald-700/50 mb-3">
|
||||
@@ -629,6 +637,7 @@ function TaskSelectScreen({
|
||||
onSelectTask,
|
||||
error,
|
||||
submitting,
|
||||
logoUrl,
|
||||
}: {
|
||||
t: Translations;
|
||||
tasks: TimeTaskField[];
|
||||
@@ -636,12 +645,13 @@ function TaskSelectScreen({
|
||||
onSelectTask: (name: string) => void;
|
||||
error: string | null;
|
||||
submitting: boolean;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="text-center">
|
||||
<span className="relative inline-block h-14 w-auto">
|
||||
<Image src={OLATHE_SWEET_LOGO_DARK} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" />
|
||||
{logoUrl ? <Image src={logoUrl} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" /> : null}
|
||||
</span>
|
||||
<h1 className="text-2xl font-black text-white">{t.title}</h1>
|
||||
<p className="text-sm text-zinc-400 mt-1">{t.select_task}</p>
|
||||
@@ -709,6 +719,7 @@ function ClockOutForm({
|
||||
onBack: () => void;
|
||||
error: string | null;
|
||||
submitting: boolean;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -820,6 +831,7 @@ function ClockedOutScreen({
|
||||
payPeriod: PayPeriodHours | null;
|
||||
clockOutResult: { clock_out: string; total_minutes: number };
|
||||
onBackToPin: () => void;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -875,11 +887,13 @@ function PinEntry({
|
||||
onSubmit,
|
||||
error,
|
||||
submitting,
|
||||
logoUrl,
|
||||
}: {
|
||||
t: Translations;
|
||||
onSubmit: (pin: string) => void;
|
||||
error: string | null;
|
||||
submitting: boolean;
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
const [pin, setPin] = useState("");
|
||||
|
||||
@@ -893,7 +907,7 @@ function PinEntry({
|
||||
<div className="space-y-8">
|
||||
<div className="text-center">
|
||||
<span className="relative inline-block h-14 w-auto">
|
||||
<Image src={OLATHE_SWEET_LOGO_DARK} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" />
|
||||
{logoUrl ? <Image src={logoUrl} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" /> : null}
|
||||
</span>
|
||||
<h1 className="text-2xl font-black text-white">{t.title}</h1>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user