mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 07:08:09 -05:00
Merge branch 'dev' into toran/open-2839-aiimagecustomizerblock-sends-raw-file-paths-instead-of-valid
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)" },
|
||||
];
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ export function useSupabase() {
|
||||
return logOut({
|
||||
options,
|
||||
api,
|
||||
router,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user