Merge branch 'dev' into toran/open-2839-aiimagecustomizerblock-sends-raw-file-paths-instead-of-valid

This commit is contained in:
Nicholas Tindle
2025-11-26 14:03:41 -06:00
committed by GitHub
14 changed files with 453 additions and 131 deletions

View File

@@ -0,0 +1,48 @@
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { Separator } from "@radix-ui/react-separator";
export function ProfileLoading() {
return (
<div className="flex flex-col items-center justify-center px-4">
<div className="w-full min-w-[800px] px-4 sm:px-8">
<Skeleton className="mb-6 h-[35px] w-32 sm:mb-8" />
<div className="mb-8 sm:mb-12">
<div className="mb-8 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
<Skeleton className="h-[130px] w-[130px] rounded-full" />
<Skeleton className="mt-11 h-[43px] w-32 rounded-[22px]" />
</div>
<div className="space-y-4 sm:space-y-6">
<div className="w-full space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full rounded-[55px]" />
</div>
<div className="w-full space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-10 w-full rounded-[55px]" />
</div>
<div className="w-full space-y-2">
<Skeleton className="h-4 w-12" />
<Skeleton className="h-[220px] w-full rounded-2xl" />
</div>
<section className="mb-8">
<Skeleton className="mb-4 h-6 w-32" />
<Skeleton className="mb-6 h-4 w-64" />
<div className="space-y-4 sm:space-y-6">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="w-full space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-10 w-full rounded-[55px]" />
</div>
))}
</div>
</section>
<Separator />
<div className="flex h-[50px] items-center justify-end gap-3 py-8">
<Skeleton className="h-[50px] w-32 rounded-[35px]" />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,23 +1,58 @@
import React from "react";
import { Metadata } from "next/types";
import { redirect } from "next/navigation";
import BackendAPI from "@/lib/autogpt-server-api";
"use client";
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { ProfileInfoForm } from "@/components/__legacy__/ProfileInfoForm";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { ProfileLoading } from "./ProfileLoading";
// Force dynamic rendering to avoid static generation issues with cookies
export const dynamic = "force-dynamic";
export default function UserProfilePage() {
const { user } = useSupabase();
export const metadata: Metadata = { title: "Profile - AutoGPT Platform" };
export default async function UserProfilePage(): Promise<React.ReactElement> {
const api = new BackendAPI();
const profile = await api.getStoreProfile().catch((error) => {
console.error("Error fetching profile:", error);
return null;
const {
data: profile,
isLoading,
isError,
error,
refetch,
} = useGetV2GetUserProfile<ProfileDetails | null>({
query: {
enabled: !!user,
select: (res) => {
if (res.status === 200) {
return {
...res.data,
avatar_url: res.data.avatar_url ?? "",
};
}
return null;
},
},
});
if (!profile) {
redirect("/login");
if (isError) {
return (
<div className="flex flex-col items-center justify-center px-4">
<ErrorCard
responseError={
error
? {
detail: error.detail,
}
: undefined
}
context="profile"
onRetry={() => {
void refetch();
}}
/>
</div>
);
}
if (isLoading || !user || !profile) {
return <ProfileLoading />;
}
return (

View File

@@ -1,7 +1,7 @@
"use client";
import { Separator } from "@/components/__legacy__/ui/separator";
import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference";
import { Separator } from "@/components/__legacy__/ui/separator";
import { User } from "@supabase/supabase-js";
import { EmailForm } from "./components/EmailForm/EmailForm";
import { NotificationForm } from "./components/NotificationForm/NotificationForm";
@@ -18,6 +18,8 @@ export function SettingsForm({
user,
timezone,
}: SettingsFormProps) {
if (!user || !preferences) return null;
return (
<div className="flex flex-col gap-8">
<EmailForm user={user} />

View File

@@ -1,22 +1,11 @@
"use client";
import * as React from "react";
import { useTimezoneForm } from "./useTimezoneForm";
import { User } from "@supabase/supabase-js";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/__legacy__/ui/card";
import { Button } from "@/components/atoms/Button/Button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/__legacy__/ui/select";
import {
Form,
FormControl,
@@ -25,48 +14,26 @@ import {
FormLabel,
FormMessage,
} from "@/components/__legacy__/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/__legacy__/ui/select";
import { Button } from "@/components/atoms/Button/Button";
import { User } from "@supabase/supabase-js";
import * as React from "react";
import { TIMEZONES } from "./helpers";
import { useTimezoneForm } from "./useTimezoneForm";
type TimezoneFormProps = {
type Props = {
user: User;
currentTimezone?: string;
};
// Common timezones list - can be expanded later
const TIMEZONES = [
{ value: "UTC", label: "UTC (Coordinated Universal Time)" },
{ value: "America/New_York", label: "Eastern Time (US & Canada)" },
{ value: "America/Chicago", label: "Central Time (US & Canada)" },
{ value: "America/Denver", label: "Mountain Time (US & Canada)" },
{ value: "America/Los_Angeles", label: "Pacific Time (US & Canada)" },
{ value: "America/Phoenix", label: "Arizona (US)" },
{ value: "America/Anchorage", label: "Alaska (US)" },
{ value: "Pacific/Honolulu", label: "Hawaii (US)" },
{ value: "Europe/London", label: "London (UK)" },
{ value: "Europe/Paris", label: "Paris (France)" },
{ value: "Europe/Berlin", label: "Berlin (Germany)" },
{ value: "Europe/Moscow", label: "Moscow (Russia)" },
{ value: "Asia/Dubai", label: "Dubai (UAE)" },
{ value: "Asia/Kolkata", label: "India Standard Time" },
{ value: "Asia/Shanghai", label: "China Standard Time" },
{ value: "Asia/Tokyo", label: "Tokyo (Japan)" },
{ value: "Asia/Seoul", label: "Seoul (South Korea)" },
{ value: "Asia/Singapore", label: "Singapore" },
{ value: "Australia/Sydney", label: "Sydney (Australia)" },
{ value: "Australia/Melbourne", label: "Melbourne (Australia)" },
{ value: "Pacific/Auckland", label: "Auckland (New Zealand)" },
{ value: "America/Toronto", label: "Toronto (Canada)" },
{ value: "America/Vancouver", label: "Vancouver (Canada)" },
{ value: "America/Mexico_City", label: "Mexico City (Mexico)" },
{ value: "America/Sao_Paulo", label: "São Paulo (Brazil)" },
{ value: "America/Buenos_Aires", label: "Buenos Aires (Argentina)" },
{ value: "Africa/Cairo", label: "Cairo (Egypt)" },
{ value: "Africa/Johannesburg", label: "Johannesburg (South Africa)" },
];
export function TimezoneForm({
user,
currentTimezone = "not-set",
}: TimezoneFormProps) {
export function TimezoneForm({ user, currentTimezone = "not-set" }: Props) {
console.log("currentTimezone", currentTimezone);
// If timezone is not set, try to detect it from the browser
const effectiveTimezone = React.useMemo(() => {
if (currentTimezone === "not-set") {
@@ -120,7 +87,7 @@ export function TimezoneForm({
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
<Button type="submit" disabled={isLoading} size="small">
{isLoading ? "Saving..." : "Save timezone"}
</Button>
</form>

View File

@@ -0,0 +1,123 @@
export const TIMEZONES = [
{ value: "UTC", label: "UTC (Coordinated Universal Time)" },
{ value: "America/Adak", label: "Adak (US - Aleutian Islands)" },
{ value: "America/Anchorage", label: "Anchorage (US - Alaska)" },
{ value: "America/Los_Angeles", label: "Los Angeles (US - Pacific)" },
{ value: "America/Tijuana", label: "Tijuana (Mexico - Pacific)" },
{ value: "America/Phoenix", label: "Phoenix (US - Arizona)" },
{ value: "America/Denver", label: "Denver (US - Mountain)" },
{ value: "America/Chicago", label: "Chicago (US - Central)" },
{ value: "America/New_York", label: "New York (US - Eastern)" },
{ value: "America/Toronto", label: "Toronto (Canada - Eastern)" },
{ value: "America/Halifax", label: "Halifax (Canada - Atlantic)" },
{ value: "America/St_Johns", label: "St. John's (Canada - Newfoundland)" },
{ value: "America/Caracas", label: "Caracas (Venezuela)" },
{ value: "America/Bogota", label: "Bogotá (Colombia)" },
{ value: "America/Lima", label: "Lima (Peru)" },
{ value: "America/Santiago", label: "Santiago (Chile)" },
{ value: "America/La_Paz", label: "La Paz (Bolivia)" },
{ value: "America/Asuncion", label: "Asunción (Paraguay)" },
{ value: "America/Montevideo", label: "Montevideo (Uruguay)" },
{ value: "America/Buenos_Aires", label: "Buenos Aires (Argentina)" },
{ value: "America/Sao_Paulo", label: "São Paulo (Brazil)" },
{ value: "America/Manaus", label: "Manaus (Brazil - Amazon)" },
{ value: "America/Fortaleza", label: "Fortaleza (Brazil - Northeast)" },
{ value: "America/Mexico_City", label: "Mexico City (Mexico)" },
{ value: "America/Guatemala", label: "Guatemala" },
{ value: "America/Costa_Rica", label: "Costa Rica" },
{ value: "America/Panama", label: "Panama" },
{ value: "America/Havana", label: "Havana (Cuba)" },
{ value: "America/Jamaica", label: "Jamaica" },
{ value: "America/Port-au-Prince", label: "Port-au-Prince (Haiti)" },
{
value: "America/Santo_Domingo",
label: "Santo Domingo (Dominican Republic)",
},
{ value: "America/Puerto_Rico", label: "Puerto Rico" },
{ value: "Atlantic/Azores", label: "Azores (Portugal)" },
{ value: "Atlantic/Cape_Verde", label: "Cape Verde" },
{ value: "Europe/London", label: "London (UK)" },
{ value: "Europe/Dublin", label: "Dublin (Ireland)" },
{ value: "Europe/Lisbon", label: "Lisbon (Portugal)" },
{ value: "Europe/Madrid", label: "Madrid (Spain)" },
{ value: "Europe/Paris", label: "Paris (France)" },
{ value: "Europe/Brussels", label: "Brussels (Belgium)" },
{ value: "Europe/Amsterdam", label: "Amsterdam (Netherlands)" },
{ value: "Europe/Berlin", label: "Berlin (Germany)" },
{ value: "Europe/Rome", label: "Rome (Italy)" },
{ value: "Europe/Vienna", label: "Vienna (Austria)" },
{ value: "Europe/Zurich", label: "Zurich (Switzerland)" },
{ value: "Europe/Prague", label: "Prague (Czech Republic)" },
{ value: "Europe/Warsaw", label: "Warsaw (Poland)" },
{ value: "Europe/Stockholm", label: "Stockholm (Sweden)" },
{ value: "Europe/Oslo", label: "Oslo (Norway)" },
{ value: "Europe/Copenhagen", label: "Copenhagen (Denmark)" },
{ value: "Europe/Helsinki", label: "Helsinki (Finland)" },
{ value: "Europe/Athens", label: "Athens (Greece)" },
{ value: "Europe/Bucharest", label: "Bucharest (Romania)" },
{ value: "Europe/Sofia", label: "Sofia (Bulgaria)" },
{ value: "Europe/Budapest", label: "Budapest (Hungary)" },
{ value: "Europe/Belgrade", label: "Belgrade (Serbia)" },
{ value: "Europe/Zagreb", label: "Zagreb (Croatia)" },
{ value: "Europe/Moscow", label: "Moscow (Russia)" },
{ value: "Europe/Kiev", label: "Kyiv (Ukraine)" },
{ value: "Europe/Istanbul", label: "Istanbul (Turkey)" },
{ value: "Asia/Dubai", label: "Dubai (UAE)" },
{ value: "Asia/Muscat", label: "Muscat (Oman)" },
{ value: "Asia/Kuwait", label: "Kuwait" },
{ value: "Asia/Riyadh", label: "Riyadh (Saudi Arabia)" },
{ value: "Asia/Baghdad", label: "Baghdad (Iraq)" },
{ value: "Asia/Tehran", label: "Tehran (Iran)" },
{ value: "Asia/Kabul", label: "Kabul (Afghanistan)" },
{ value: "Asia/Karachi", label: "Karachi (Pakistan)" },
{ value: "Asia/Tashkent", label: "Tashkent (Uzbekistan)" },
{ value: "Asia/Dhaka", label: "Dhaka (Bangladesh)" },
{ value: "Asia/Kolkata", label: "Kolkata (India)" },
{ value: "Asia/Kathmandu", label: "Kathmandu (Nepal)" },
{ value: "Asia/Colombo", label: "Colombo (Sri Lanka)" },
{ value: "Asia/Yangon", label: "Yangon (Myanmar)" },
{ value: "Asia/Bangkok", label: "Bangkok (Thailand)" },
{ value: "Asia/Ho_Chi_Minh", label: "Ho Chi Minh City (Vietnam)" },
{ value: "Asia/Jakarta", label: "Jakarta (Indonesia - Western)" },
{ value: "Asia/Makassar", label: "Makassar (Indonesia - Central)" },
{ value: "Asia/Jayapura", label: "Jayapura (Indonesia - Eastern)" },
{ value: "Asia/Manila", label: "Manila (Philippines)" },
{ value: "Asia/Singapore", label: "Singapore" },
{ value: "Asia/Kuala_Lumpur", label: "Kuala Lumpur (Malaysia)" },
{ value: "Asia/Hong_Kong", label: "Hong Kong" },
{ value: "Asia/Shanghai", label: "Shanghai (China)" },
{ value: "Asia/Taipei", label: "Taipei (Taiwan)" },
{ value: "Asia/Seoul", label: "Seoul (South Korea)" },
{ value: "Asia/Tokyo", label: "Tokyo (Japan)" },
{ value: "Asia/Vladivostok", label: "Vladivostok (Russia)" },
{ value: "Asia/Yakutsk", label: "Yakutsk (Russia)" },
{ value: "Asia/Irkutsk", label: "Irkutsk (Russia)" },
{ value: "Asia/Yekaterinburg", label: "Yekaterinburg (Russia)" },
{ value: "Australia/Perth", label: "Perth (Australia - Western)" },
{ value: "Australia/Darwin", label: "Darwin (Australia - Northern)" },
{ value: "Australia/Adelaide", label: "Adelaide (Australia - Central)" },
{ value: "Australia/Brisbane", label: "Brisbane (Australia - Eastern)" },
{ value: "Australia/Sydney", label: "Sydney (Australia - Eastern)" },
{ value: "Australia/Melbourne", label: "Melbourne (Australia - Eastern)" },
{ value: "Australia/Hobart", label: "Hobart (Australia - Eastern)" },
{ value: "Pacific/Auckland", label: "Auckland (New Zealand)" },
{ value: "Pacific/Fiji", label: "Fiji" },
{ value: "Pacific/Guam", label: "Guam" },
{ value: "Pacific/Honolulu", label: "Honolulu (US - Hawaii)" },
{ value: "Pacific/Samoa", label: "Samoa" },
{ value: "Pacific/Tahiti", label: "Tahiti (French Polynesia)" },
{ value: "Africa/Cairo", label: "Cairo (Egypt)" },
{ value: "Africa/Johannesburg", label: "Johannesburg (South Africa)" },
{ value: "Africa/Lagos", label: "Lagos (Nigeria)" },
{ value: "Africa/Nairobi", label: "Nairobi (Kenya)" },
{ value: "Africa/Casablanca", label: "Casablanca (Morocco)" },
{ value: "Africa/Algiers", label: "Algiers (Algeria)" },
{ value: "Africa/Tunis", label: "Tunis (Tunisia)" },
{ value: "Africa/Addis_Ababa", label: "Addis Ababa (Ethiopia)" },
{ value: "Africa/Dar_es_Salaam", label: "Dar es Salaam (Tanzania)" },
{ value: "Africa/Kampala", label: "Kampala (Uganda)" },
{ value: "Africa/Khartoum", label: "Khartoum (Sudan)" },
{ value: "Africa/Accra", label: "Accra (Ghana)" },
{ value: "Africa/Abidjan", label: "Abidjan (Ivory Coast)" },
{ value: "Africa/Dakar", label: "Dakar (Senegal)" },
];

View File

@@ -1,4 +1,5 @@
"use client";
import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm";
import { useTimezoneDetection } from "@/app/(platform)/profile/(user)/settings/useTimezoneDetection";
import {
@@ -6,49 +7,67 @@ import {
useGetV1GetUserTimezone,
} from "@/app/api/__generated__/endpoints/auth/auth";
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { redirect } from "next/navigation";
import { useEffect } from "react";
import SettingsLoading from "./loading";
export default function SettingsPage() {
const { user } = useSupabase();
const {
data: preferences,
isError: preferencesError,
isLoading: preferencesLoading,
error: preferencesErrorData,
refetch: refetchPreferences,
} = useGetV1GetNotificationPreferences({
query: { select: (res) => (res.status === 200 ? res.data : null) },
query: {
enabled: !!user,
select: (res) => (res.status === 200 ? res.data : null),
},
});
const { data: timezone, isLoading: timezoneLoading } =
useGetV1GetUserTimezone({
query: {
enabled: !!user,
select: (res) => {
return res.status === 200 ? String(res.data.timezone) : "not-set";
},
},
});
useTimezoneDetection(timezone);
const { user, isUserLoading } = useSupabase();
useTimezoneDetection(!!user ? timezone : undefined);
useEffect(() => {
document.title = "Settings AutoGPT Platform";
}, []);
if (preferencesLoading || isUserLoading || timezoneLoading) {
if (preferencesError) {
return (
<div className="container max-w-2xl py-10">
<ErrorCard
responseError={
preferencesErrorData
? {
detail: preferencesErrorData.detail,
}
: undefined
}
context="settings"
onRetry={() => {
void refetchPreferences();
}}
/>
</div>
);
}
if (preferencesLoading || timezoneLoading || !user || !preferences) {
return <SettingsLoading />;
}
if (!user) {
redirect("/login");
}
if (preferencesError || !preferences || !preferences.preferences) {
return "Error..."; // TODO: Will use a Error reusable components from Block Menu redesign
}
return (
<div className="container max-w-2xl space-y-6 py-10">
<div className="flex flex-col gap-2">

View File

@@ -6,6 +6,7 @@ import "./globals.css";
import { Providers } from "@/app/providers";
import { CookieConsentBanner } from "@/components/molecules/CookieConsentBanner/CookieConsentBanner";
import { ErrorBoundary } from "@/components/molecules/ErrorBoundary/ErrorBoundary";
import TallyPopupSimple from "@/components/molecules/TallyPoup/TallyPopup";
import { Toaster } from "@/components/molecules/Toast/toaster";
import { SetupAnalytics } from "@/services/analytics";
@@ -54,29 +55,31 @@ export default async function RootLayout({
/>
</head>
<body>
<Providers
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
// enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
{children}
<TallyPopupSimple />
<VercelAnalyticsWrapper />
<ErrorBoundary context="application">
<Providers
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
// enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
{children}
<TallyPopupSimple />
<VercelAnalyticsWrapper />
{/* React Query DevTools is only available in development */}
{process.env.NEXT_PUBLIC_REACT_QUERY_DEVTOOL && (
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition={"bottom-left"}
/>
)}
</div>
<Toaster />
<CookieConsentBanner />
</Providers>
{/* React Query DevTools is only available in development */}
{process.env.NEXT_PUBLIC_REACT_QUERY_DEVTOOL && (
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition={"bottom-left"}
/>
)}
</div>
<Toaster />
<CookieConsentBanner />
</Providers>
</ErrorBoundary>
</body>
</html>
);

View File

@@ -3,15 +3,16 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/__legacy__/ui/popover";
import Link from "next/link";
import * as React from "react";
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
import { AccountLogoutOption } from "./components/AccountLogoutOption";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import Avatar, {
AvatarFallback,
AvatarImage,
} from "@/components/atoms/Avatar/Avatar";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import Link from "next/link";
import * as React from "react";
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
import { AccountLogoutOption } from "./components/AccountLogoutOption";
interface Props {
userName?: string;
@@ -19,6 +20,7 @@ interface Props {
avatarSrc?: string;
hideNavBarUsername?: boolean;
menuItemGroups: MenuItemGroup[];
isLoading?: boolean;
}
export function AccountMenu({
@@ -26,6 +28,7 @@ export function AccountMenu({
userEmail,
avatarSrc,
menuItemGroups,
isLoading = false,
}: Props) {
const popupId = React.useId();
@@ -63,15 +66,24 @@ export function AccountMenu({
</AvatarFallback>
</Avatar>
<div className="relative flex h-[47px] w-[173px] flex-col items-start justify-center gap-1">
<div className="max-w-[10.5rem] truncate font-sans text-base font-semibold leading-none text-white dark:text-neutral-200">
{userName}
</div>
<div
data-testid="account-menu-user-email"
className="max-w-[10.5rem] truncate font-sans text-base font-normal leading-none text-white dark:text-neutral-400"
>
{userEmail}
</div>
{isLoading || !userName || !userEmail ? (
<>
<Skeleton className="h-4 w-24 bg-white/40" />
<Skeleton className="h-4 w-32 bg-white/40" />
</>
) : (
<>
<div className="max-w-[10.5rem] truncate font-sans text-base font-semibold leading-none text-white dark:text-neutral-200">
{userName}
</div>
<div
data-testid="account-menu-user-email"
className="max-w-[10.5rem] truncate font-sans text-base font-normal leading-none text-white dark:text-neutral-400"
>
{userEmail}
</div>
</>
)}
</div>
</div>

View File

@@ -5,17 +5,20 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils";
import * as Sentry from "@sentry/nextjs";
import { useRouter } from "next/navigation";
import { useState } from "react";
export function AccountLogoutOption() {
const [isLoggingOut, setIsLoggingOut] = useState(false);
const supabase = useSupabase();
const router = useRouter();
const { toast } = useToast();
async function handleLogout() {
setIsLoggingOut(true);
try {
await supabase.logOut();
router.push("/login");
} catch (e) {
Sentry.captureException(e);
toast({
@@ -25,7 +28,9 @@ export function AccountLogoutOption() {
variant: "destructive",
});
} finally {
setIsLoggingOut(false);
setTimeout(() => {
setIsLoggingOut(false);
}, 3000);
}
}

View File

@@ -26,12 +26,19 @@ export function NavbarView({ isLoggedIn, previewBranchName }: NavbarViewProps) {
const dynamicMenuItems = getAccountMenuItems(user?.role);
const isChatEnabled = useGetFlag(Flag.CHAT);
const { data: profile } = useGetV2GetUserProfile({
query: {
select: (res) => (res.status === 200 ? res.data : null),
enabled: isLoggedIn,
const { data: profile, isLoading: isProfileLoading } = useGetV2GetUserProfile(
{
query: {
select: (res) => (res.status === 200 ? res.data : null),
enabled: isLoggedIn && !!user,
// Include user ID in query key to ensure cache invalidation when user changes
queryKey: ["/api/store/profile", user?.id],
},
},
});
);
const { isUserLoading } = useSupabase();
const isLoadingProfile = isProfileLoading || isUserLoading;
const linksWithChat = useMemo(() => {
const chatLink = { name: "Chat", href: "/chat" };
@@ -84,6 +91,7 @@ export function NavbarView({ isLoggedIn, previewBranchName }: NavbarViewProps) {
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
menuItemGroups={dynamicMenuItems}
isLoading={isLoadingProfile}
/>
</div>
</div>

View File

@@ -0,0 +1,78 @@
"use client";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import * as Sentry from "@sentry/nextjs";
import { Component, type ReactNode } from "react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
context?: string;
onReset?: () => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
tags: {
errorBoundary: "true",
context: this.props.context || "application",
},
});
}
handleReset = () => {
this.setState({ hasError: false, error: null });
if (this.props.onReset) {
this.props.onReset();
}
};
render() {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
<div className="relative w-full max-w-xl">
<ErrorCard
responseError={{
message:
this.state.error.message ||
"An unexpected error occurred. Our team has been notified and is working to resolve the issue.",
}}
context={this.props.context || "application"}
onRetry={this.handleReset}
/>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -51,9 +51,11 @@ export async function fetchUser(): Promise<FetchUserResult> {
const { user, error } = await getCurrentUser();
if (error || !user) {
// Only mark as loaded if we got an explicit error (not just no user)
// This allows retrying when cookies aren't ready yet after login
return {
user: null,
hasLoadedUser: true,
hasLoadedUser: !!error, // Only true if there was an error, not just no user
isUserLoading: false,
};
}
@@ -68,7 +70,7 @@ export async function fetchUser(): Promise<FetchUserResult> {
console.error("Get user error:", error);
return {
user: null,
hasLoadedUser: true,
hasLoadedUser: true, // Error means we tried and failed, so mark as loaded
isUserLoading: false,
};
}

View File

@@ -44,7 +44,6 @@ export function useSupabase() {
return logOut({
options,
api,
router,
});
}

View File

@@ -72,10 +72,29 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
if (!initializationPromise) {
initializationPromise = (async () => {
if (!get().hasLoadedUser) {
// Always fetch user if we haven't loaded it yet, or if user is null but hasLoadedUser is true
// This handles the case where hasLoadedUser might be stale after logout/login
if (!get().hasLoadedUser || !get().user) {
set({ isUserLoading: true });
const result = await fetchUser();
set(result);
// If fetchUser didn't return a user, validate the session to ensure we have the latest state
// This handles race conditions after login where cookies might not be immediately available
if (!result.user) {
const validationResult = await validateSessionHelper({
pathname: params.pathname,
currentUser: null,
});
if (validationResult.user && validationResult.isValid) {
set({
user: validationResult.user,
hasLoadedUser: true,
isUserLoading: false,
});
}
}
} else {
set({ isUserLoading: false });
}
@@ -104,7 +123,6 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
}
async function logOut(params?: LogOutParams): Promise<void> {
const router = params?.router ?? get().routerRef;
const api = params?.api ?? get().apiRef;
const options = params?.options ?? {};
@@ -122,17 +140,20 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
broadcastLogout();
// Clear React Query cache to prevent stale data from old user
if (typeof window !== "undefined") {
const { getQueryClient } = await import("@/lib/react-query/queryClient");
const queryClient = getQueryClient();
queryClient.clear();
}
set({
user: null,
hasLoadedUser: false,
isUserLoading: false,
});
const result = await serverLogout(options);
if (result.success && router) {
router.push("/login");
}
await serverLogout(options);
}
async function validateSessionInternal(