Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/blocks/__global/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Props {
name?: string | null;
email: string;
plan?: string | null;
avatarUrl?: string | null;
};
}

Expand All @@ -33,7 +34,7 @@ export function AppSidebar({ user }: Props) {
{ to: "/app/sales", label: "Sales Log", icon: <IconReceipt size={20} /> },
{ to: "/app/expenses", label: "Expenses", icon: <IconWallet size={20} /> },
{ to: "/app/ai-insights", label: "AI Insights", icon: <IconBrain size={20} /> },
{ to: "/app/settings", label: "Settings", icon: <IconSettings size={20} /> },
{ to: "/settings", label: "Settings", icon: <IconSettings size={20} /> },
];

return (
Expand Down Expand Up @@ -62,7 +63,11 @@ export function AppSidebar({ user }: Props) {
<div className={styles.footer}>
<div className={styles.userProfile}>
<div className={styles.avatar}>
{user.name ? user.name.charAt(0).toUpperCase() : user.email.charAt(0).toUpperCase()}
{user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.name || user.email} className={styles.avatarImage} />
) : (
user.name ? user.name.charAt(0).toUpperCase() : user.email.charAt(0).toUpperCase()
)}
</div>
<div className={styles.userInfo}>
<span className={styles.userName}>{user.name || "User"}</span>
Expand Down
2 changes: 1 addition & 1 deletion app/blocks/__global/navigation-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function NavigationBar({ className, appMode }: Props) {
</div>
<div className={styles.spacer} />
<div className={styles.actions}>
<Link to="/app/settings" className={styles.btnGhost}>
<Link to="/settings" className={styles.btnGhost}>
<IconSettings size={18} />
</Link>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/blocks/ai-insights/plan-gate-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function PlanGateMessage({ className }: Props) {
Upgrade to Pro to get AI-powered price analysis and buy/sell recommendations for every item in your inventory.
Powered by GPT-4o mini with confidence scores and target prices.
</p>
<Link to="/app/settings/billing" className={styles.btn}>Upgrade to Pro &mdash; $12/mo</Link>
<Link to="/settings/billing" className={styles.btn}>Upgrade to Pro &mdash; $12/mo</Link>
</div>
);
}
2 changes: 1 addition & 1 deletion app/blocks/dashboard/expense-categories-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function ExpenseCategoriesChart({ expenses }: Props) {
))}
</Pie>
<Tooltip
formatter={(value: any) => [`$${Number(value).toFixed(2)}`, "Amount"]}
formatter={(value: any) => [`$${Number(value).toFixed(2)}`, "Amount"]} //changed the code to avoid merge conflicts
contentStyle={{
backgroundColor: "var(--color-surface)",
borderColor: "var(--color-surface-border)",
Expand Down
32 changes: 22 additions & 10 deletions app/blocks/price-alerts/alert-history.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import styles from "./alert-history.module.css";

interface Props { className?: string; alerts?: any[]; }
interface AlertItem {
id: string;
sku: string;
size: string;
productName: string;
marketplace: string;
targetPrice: number;
direction: string;
notificationChannel: string;
isActive: boolean;
triggeredAt: Date | null;
createdAt: Date;
userId: string;
}

const history = [
{ desc: "Yeezy 350 Zebra (GOAT) dropped below $200 — alert triggered at $195", time: "May 10, 2024 2:14 PM" },
];
interface Props { className?: string; alerts?: AlertItem[]; }
//changed the code to avoid merge conflicts

export function AlertHistory({ className }: Props) {
export function AlertHistory({ className, alerts = [] }: Props) {
return (
<div className={[styles.section, className].filter(Boolean).join(" ")}>
<div className={styles.title}>Alert History</div>
{history.length === 0 ? (
{alerts.length === 0 ? (
<p className={styles.empty}>No alerts have been triggered yet.</p>
) : (
history.map((h, i) => (
<div key={i} className={styles.item}>
alerts.map((alert) => (
<div key={alert.id} className={styles.item}>
<div className={styles.dot} />
<div className={styles.desc}>{h.desc}</div>
<div className={styles.time}>{h.time}</div>
<div className={styles.desc}>{`${alert.productName} (${alert.marketplace}) triggered at $${alert.targetPrice.toFixed(2)}`}</div>
<div className={styles.time}>{alert.triggeredAt?.toLocaleString() || "Recently triggered"}</div>
</div>
))
)}
Expand Down
2 changes: 1 addition & 1 deletion app/blocks/price-alerts/plan-limit-warning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function PlanLimitWarning({ className, show = false }: Props) {
return (
<div className={[styles.banner, className].filter(Boolean).join(" ")}>
<span className={styles.text}>⚠️ You&apos;ve reached your 5-alert limit on the Free plan. Upgrade to Pro for 25 alerts.</span>
<Link to="/app/settings/billing" className={styles.upgradeBtn}>Upgrade to Pro</Link>
<Link to="/settings/billing" className={styles.upgradeBtn}>Upgrade to Pro</Link>
</div>
);
}
7 changes: 5 additions & 2 deletions app/blocks/settings/account-settings.module.css
Original file line number Diff line number Diff line change
@@ -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; }
Expand Down
19 changes: 16 additions & 3 deletions app/blocks/settings/account-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,24 @@ export function AccountSettings({ className, user }: Props) {
<div className={[styles.section, className].filter(Boolean).join(" ")}>
<h2 className={styles.title}>Account Settings</h2>
<div className={styles.avatarRow}>
<div className={styles.avatar}>{user.name ? user.name.substring(0, 2).toUpperCase() : user.email.substring(0, 2).toUpperCase()}</div>
<div className={styles.avatar}>
{user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.name || user.email} className={styles.avatarImage} />
) : (
user.name ? user.name.substring(0, 2).toUpperCase() : user.email.substring(0, 2).toUpperCase()
)}
</div>
<div>
<label className={styles.uploadBtn}>
Upload Photo
<input className={styles.uploadInput} type="file" name="avatar" accept="image/*" />
</label>
<div className={styles.helperText}>PNG, JPG, or WEBP up to 5MB.</div>
</div>
</div>
<Form method="post">
<Form method="post" encType="multipart/form-data">
<input type="hidden" name="intent" value="update-profile" />
<div className={styles.field}><label className={styles.label}>Full Name</label><input name="name" className={styles.input} defaultValue={user.name || ""} /></div>
<div className={styles.field}><label className={styles.label}>Display Name</label><input name="name" className={styles.input} defaultValue={user.name || ""} /></div>
<div className={styles.field}><label className={styles.label}>Email</label><input className={styles.input} defaultValue={user.email} type="email" disabled /></div>
<div className={styles.field}><label className={styles.label}>Phone</label><input name="phone" className={styles.input} defaultValue={user.phone || ""} type="tel" /></div>
<button type="submit" className={styles.saveBtn}>Save Changes</button>
Expand Down
2 changes: 1 addition & 1 deletion app/blocks/settings/billing-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function BillingSection({ className, user }: Props) {
</div>
<div className={styles.actions}>
{user.plan === 'FREE' && (
<Link to="/app/settings/billing" className={styles.upgradeBtn}>Upgrade to Pro</Link>
<Link to="/settings/billing" className={styles.upgradeBtn}>Upgrade to Pro</Link>
)}
<button className={styles.manageBtn}>Manage Subscription</button>
</div>
Expand Down
13 changes: 5 additions & 8 deletions app/blocks/settings/preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,16 @@ export function Preferences({ className, user }: Props) {
<label className={styles.label}>Currency</label>
<select name="currency" defaultValue={user?.currency || "USD"} className={styles.select}>
<option value="USD">USD ($)</option>
<option value="CAD">CAD (CA$)</option>
<option value="GBP">GBP (£)</option>
<option value="EUR">EUR (€)</option>
<option value="AUD">AUD (A$)</option>
<option value="JPY">JPY (¥)</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
<div className={styles.field}>
<label className={styles.label}>Theme</label>
<select name="theme" defaultValue={user?.theme || "dark"} className={styles.select}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
<select name="theme" defaultValue={user?.theme || "LIGHT"} className={styles.select}>
<option value="LIGHT">Light</option>
<option value="DARK">Dark</option>
<option value="UNICORN">Unicorn</option>
</select>
</div>
<div className={styles.field}>
Expand Down
2 changes: 1 addition & 1 deletion app/blocks/settings/team-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function TeamSection({ className, user }: Props) {
<div className={styles.gate}>
Team collaboration is a Business plan feature. Upgrade to invite up to 5 team members and share your inventory.
<br />
<Link to="/app/settings/billing" className={styles.gateBtn}>Upgrade to Business</Link>
<Link to="/settings/billing" className={styles.gateBtn}>Upgrade to Business</Link>
</div>
</div>
);
Expand Down
10 changes: 5 additions & 5 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
]),
]),
Expand Down
6 changes: 6 additions & 0 deletions app/routes/+types/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";

export namespace Route {
export type LoaderArgs = LoaderFunctionArgs;
export type ActionArgs = ActionFunctionArgs;
}
3 changes: 2 additions & 1 deletion app/routes/api.stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion app/routes/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" } };
Expand Down
17 changes: 16 additions & 1 deletion app/routes/price-alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -91,7 +106,7 @@ export default function PriceAlertsPage() {
<PlanLimitWarning />
{showCreate && <CreateAlertForm onClose={() => setShowCreate(false)} />}
<ActiveAlertsTable alerts={alerts} />
<AlertHistory alerts={alerts.filter((a: any) => a.triggeredAt)} />
<AlertHistory alerts={alerts.filter((a): a is AlertHistoryItem => Boolean(a.triggeredAt))} />
</div>
);
}
1 change: 1 addition & 0 deletions app/routes/settings-public.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./settings";
37 changes: 31 additions & 6 deletions app/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -111,4 +136,4 @@ export default function SettingsPage() {
</div>
</div>
);
}
}
9 changes: 6 additions & 3 deletions app/utils/supabase.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ export function getSupabaseServerClient(request: Request) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

const supabase = createServerClient(supabaseUrl, supabaseKey, {
const supabase = createServerClient(supabaseUrl, supabaseKey, {
cookies: {
getAll() {
return parseCookieHeader(request.headers.get("Cookie") ?? "") as any;
return parseCookieHeader(request.headers.get("Cookie") ?? "").map((cookie) => ({
name: cookie.name,
value: cookie.value ?? ""
}));
},
setAll(cookiesToSet: any[]) {
cookiesToSet.forEach(({ name, value, options }) =>
headers.append("Set-Cookie", serializeCookieHeader(name, value, options))
headers.append("Set-Cookie", serializeCookieHeader(name, value, options ?? {}))
);
},
},
Expand Down
13 changes: 0 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading