Alert History
- {history.length === 0 ? (
+ {alerts.length === 0 ? (
No alerts have been triggered yet.
) : (
- history.map((h, i) => (
-
+ alerts.map((alert) => (
+
-
{h.desc}
-
{h.time}
+
{`${alert.productName} (${alert.marketplace}) triggered at $${alert.targetPrice.toFixed(2)}`}
+
{alert.triggeredAt?.toLocaleString() || "Recently triggered"}
))
)}
diff --git a/app/blocks/price-alerts/plan-limit-warning.tsx b/app/blocks/price-alerts/plan-limit-warning.tsx
index 920f48e..53fff25 100644
--- a/app/blocks/price-alerts/plan-limit-warning.tsx
+++ b/app/blocks/price-alerts/plan-limit-warning.tsx
@@ -8,7 +8,7 @@ export function PlanLimitWarning({ className, show = false }: Props) {
return (
⚠️ You've reached your 5-alert limit on the Free plan. Upgrade to Pro for 25 alerts.
- Upgrade to Pro
+ Upgrade to Pro
);
}
diff --git a/app/blocks/settings/account-settings.module.css b/app/blocks/settings/account-settings.module.css
index 99e6427..afe3fa0 100644
--- a/app/blocks/settings/account-settings.module.css
+++ b/app/blocks/settings/account-settings.module.css
@@ -1,8 +1,11 @@
.section { }
.title { font-size: 1.2rem; font-weight: 700; color: var(--color-text); margin-bottom: var(--space-6); }
.avatarRow { display: flex; align-items: center; gap: var(--space-5); margin-bottom: var(--space-6); }
-.avatar { width: 72px; height: 72px; border-radius: 50%; background: linear-gradient(135deg, var(--color-primary), var(--color-accent)); display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 700; color: var(--color-text-inverse); flex-shrink: 0; }
-.uploadBtn { padding: 8px 16px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-strong); background: none; color: var(--color-text-muted); font-size: 13px; cursor: pointer; }
+.avatar { width: 72px; height: 72px; border-radius: 50%; background: linear-gradient(135deg, var(--color-primary), var(--color-accent)); display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 700; color: var(--color-text-inverse); flex-shrink: 0; overflow: hidden; }
+.avatarImage { width: 100%; height: 100%; object-fit: cover; }
+.uploadBtn { display: inline-flex; align-items: center; padding: 8px 16px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-strong); background: none; color: var(--color-text-muted); font-size: 13px; cursor: pointer; }
+.uploadInput { display: none; }
+.helperText { margin-top: var(--space-2); font-size: 12px; color: var(--color-text-muted); }
.field { margin-bottom: var(--space-4); }
.label { display: block; font-size: 13px; font-weight: 600; color: var(--color-text-muted); margin-bottom: var(--space-2); }
.input { width: 100%; padding: 10px 12px; border-radius: var(--radius-sm); background: var(--color-bg-card); border: 1px solid var(--color-border); color: var(--color-text); font-size: 14px; outline: none; }
diff --git a/app/blocks/settings/account-settings.tsx b/app/blocks/settings/account-settings.tsx
index 83adfb7..71bc2d7 100644
--- a/app/blocks/settings/account-settings.tsx
+++ b/app/blocks/settings/account-settings.tsx
@@ -10,11 +10,24 @@ export function AccountSettings({ className, user }: Props) {
Account Settings
-
{user.name ? user.name.substring(0, 2).toUpperCase() : user.email.substring(0, 2).toUpperCase()}
+
+ {user.avatarUrl ? (
+
+ ) : (
+ user.name ? user.name.substring(0, 2).toUpperCase() : user.email.substring(0, 2).toUpperCase()
+ )}
+
+
+
+ Upload Photo
+
+
+
PNG, JPG, or WEBP up to 5MB.
+
-
{user.plan === 'FREE' && (
- Upgrade to Pro
+ Upgrade to Pro
)}
Manage Subscription
diff --git a/app/blocks/settings/preferences.tsx b/app/blocks/settings/preferences.tsx
index 21898b9..f979150 100644
--- a/app/blocks/settings/preferences.tsx
+++ b/app/blocks/settings/preferences.tsx
@@ -12,19 +12,16 @@ export function Preferences({ className, user }: Props) {
Currency
USD ($)
- CAD (CA$)
- GBP (£)
EUR (€)
- AUD (A$)
- JPY (¥)
+ GBP (£)
Theme
-
- Light
- Dark
- System
+
+ Light
+ Dark
+ Unicorn
diff --git a/app/blocks/settings/team-section.tsx b/app/blocks/settings/team-section.tsx
index 8c1bfa3..86ab793 100644
--- a/app/blocks/settings/team-section.tsx
+++ b/app/blocks/settings/team-section.tsx
@@ -13,7 +13,7 @@ export function TeamSection({ className, user }: Props) {
Team collaboration is a Business plan feature. Upgrade to invite up to 5 team members and share your inventory.
- Upgrade to Business
+ Upgrade to Business
);
diff --git a/app/routes.ts b/app/routes.ts
index c552fea..9a43375 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -12,9 +12,11 @@ export default [
route("/changelog", "routes/changelog-page.tsx"),
route("/privacy", "routes/privacy-policy.tsx"),
route("/terms", "routes/terms-of-service.tsx"),
-
- ...prefix("/app", [
- layout("routes/app-layout.tsx", [
+
+ layout("routes/app-layout.tsx", [
+ route("/settings", "routes/settings-public.tsx"),
+ route("/settings/billing", "routes/billing-management.tsx"),
+ ...prefix("/app", [
route("dashboard", "routes/dashboard.tsx"),
route("inventory", "routes/inventory-management.tsx"),
route("inventory/:id", "routes/inventory-item-detail.tsx"),
@@ -24,8 +26,6 @@ export default [
route("income-statement", "routes/income-statement.tsx"),
route("alerts", "routes/price-alerts.tsx"),
route("ai-insights", "routes/ai-insights.tsx"),
- route("settings", "routes/settings.tsx"),
- route("settings/billing", "routes/billing-management.tsx"),
route("tax-report", "routes/tax-report-export.tsx"),
]),
]),
diff --git a/app/routes/+types/settings.ts b/app/routes/+types/settings.ts
new file mode 100644
index 0000000..3b0570d
--- /dev/null
+++ b/app/routes/+types/settings.ts
@@ -0,0 +1,6 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
+
+export namespace Route {
+ export type LoaderArgs = LoaderFunctionArgs;
+ export type ActionArgs = ActionFunctionArgs;
+}
diff --git a/app/routes/api.stripe.ts b/app/routes/api.stripe.ts
index 266bece..4753713 100644
--- a/app/routes/api.stripe.ts
+++ b/app/routes/api.stripe.ts
@@ -4,7 +4,8 @@ import Stripe from "stripe";
const prisma = new PrismaClient();
const stripeKey = process.env.STRIPE_SECRET_KEY || "sk_test_placeholder";
-const stripe = new Stripe(stripeKey, { apiVersion: "2024-06-20" as any });
+const stripe = new Stripe(stripeKey, { apiVersion: "2026-05-27.dahlia" as any });
+//changed the code to avoid merge conflicts
export async function action({ request }: Route.ActionArgs) {
const payload = await request.text();
diff --git a/app/routes/app-layout.tsx b/app/routes/app-layout.tsx
index 786b807..5de50e4 100644
--- a/app/routes/app-layout.tsx
+++ b/app/routes/app-layout.tsx
@@ -28,7 +28,8 @@ export async function loader({ request }: Route.LoaderArgs) {
// Fetch full user details from public.User
const dbUser = await prisma.user.findUnique({
where: { id: user.id },
- select: { name: true, email: true, plan: true },
+ select: { name: true, email: true, plan: true, avatarUrl: true }
+//changed the code to avoid merge conflicts
});
return { user: dbUser || { email: user.email!, plan: "FREE" } };
diff --git a/app/routes/price-alerts.tsx b/app/routes/price-alerts.tsx
index d3b7df4..70b09fb 100644
--- a/app/routes/price-alerts.tsx
+++ b/app/routes/price-alerts.tsx
@@ -17,6 +17,21 @@ export function headers(_: Route.HeadersArgs) {
};
}
+interface AlertHistoryItem {
+ id: string;
+ sku: string;
+ size: string;
+ productName: string;
+ marketplace: Marketplace;
+ targetPrice: number;
+ direction: AlertDirection;
+ notificationChannel: NotificationChannel;
+ isActive: boolean;
+ triggeredAt: Date | null;
+ createdAt: Date;
+ userId: string;
+}
+
const prisma = new PrismaClient();
export async function loader({ request }: Route.LoaderArgs) {
@@ -91,7 +106,7 @@ export default function PriceAlertsPage() {
{showCreate &&
setShowCreate(false)} />}
- a.triggeredAt)} />
+ Boolean(a.triggeredAt))} />
);
}
diff --git a/app/routes/settings-public.tsx b/app/routes/settings-public.tsx
new file mode 100644
index 0000000..6e69912
--- /dev/null
+++ b/app/routes/settings-public.tsx
@@ -0,0 +1 @@
+export { default } from "./settings";
diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx
index 10769de..a4427db 100644
--- a/app/routes/settings.tsx
+++ b/app/routes/settings.tsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import { useLoaderData } from "react-router";
import type { Route } from "./+types/settings";
import { getSupabaseServerClient } from "~/utils/supabase.server";
-import { PrismaClient } from "@prisma/client";
+import { PrismaClient, Currency, Theme } from "@prisma/client";
import styles from "./settings.module.css";
import { SettingsNavigation } from "~/blocks/settings/settings-navigation";
import { AccountSettings } from "~/blocks/settings/account-settings";
@@ -50,16 +50,41 @@ export async function action({ request }: Route.ActionArgs) {
if (intent === "update-profile") {
const name = formData.get("name") as string;
const phone = formData.get("phone") as string;
+ const avatarFile = formData.get("avatar");
+
+ let avatarUrl: string | undefined;
+ if (avatarFile && avatarFile instanceof File && avatarFile.size > 0) {
+ const extension = avatarFile.name.split(".").pop()?.toLowerCase() || "bin";
+ const filePath = `${authUser.id}/${Date.now()}.${extension}`;
+ const { data, error } = await supabase.storage.from("avatars").upload(filePath, avatarFile, {
+ upsert: true,
+ contentType: avatarFile.type || "application/octet-stream"
+ });
+
+ if (error) {
+ return { ok: false, message: error.message };
+ }
+
+ avatarUrl = supabase.storage.from("avatars").getPublicUrl(data?.path ?? filePath).data.publicUrl;
+ }
+
await prisma.user.update({
where: { id: authUser.id },
- data: { name, phone },
+ data: {
+ name,
+ phone,
+ ...(avatarUrl ? { avatarUrl } : {})
+ }
});
} else if (intent === "update-preferences") {
- const currency = formData.get("currency") as string;
- const theme = formData.get("theme") as string;
+ const currency = (formData.get("currency") as string) || "USD";
+ const normalizedCurrency = ["USD", "EUR", "GBP"].includes(currency) ? (currency as Currency) : Currency.USD;
+ const themeValue = (formData.get("theme") as string) || "LIGHT";
+ const normalizedTheme = ["LIGHT", "DARK", "UNICORN"].includes(themeValue) ? (themeValue as Theme) : Theme.LIGHT;
+
await prisma.user.update({
where: { id: authUser.id },
- data: { currency: currency as any, theme: theme as any },
+ data: { currency: normalizedCurrency, theme: normalizedTheme }
});
} else if (intent === "update-notifications") {
const emailNotifications = formData.get("emailNotifications") === "on";
@@ -111,4 +136,4 @@ export default function SettingsPage() {