{hasItems && isOwner && onFilesDrop && navigationSource === 'collection' && (
diff --git a/packages/ui/src/components/TopNavigation.tsx b/packages/ui/src/components/TopNavigation.tsx
index c0879ec7..7e4352be 100644
--- a/packages/ui/src/components/TopNavigation.tsx
+++ b/packages/ui/src/components/TopNavigation.tsx
@@ -16,12 +16,14 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useNavigate } from "react-router-dom";
-import { useState, useRef } from "react";
+import { useProfiles } from "@/contexts/ProfilesContext";
+import { useState, useRef, useEffect } from "react";
import { T, getCurrentLang, supportedLanguages, translate, setLanguage } from "@/i18n";
import { CreationWizardPopup } from './CreationWizardPopup';
const TopNavigation = () => {
const { user, signOut, roles } = useAuth();
+ const { fetchProfile, profiles } = useProfiles();
const { orgSlug, isOrgContext } = useOrganization();
const location = useLocation();
const navigate = useNavigate();
@@ -33,8 +35,43 @@ const TopNavigation = () => {
const authPath = isOrgContext ? `/org/${orgSlug}/auth` : '/auth';
+ useEffect(() => {
+ if (user?.id) {
+ fetchProfile(user.id);
+ }
+ }, [user?.id, fetchProfile]);
+
+ const userProfile = user ? profiles[user.id] : null;
+ const username = userProfile?.username || user?.id;
+
const isActive = (path: string) => location.pathname === path;
+ // ... (rest of component until link)
+
+ {/* Profile Grid Button - Direct to profile feed */ }
+ {
+ user && (
+
+ )
+ }
+
+ // ...
+
+
+
+
+ Profile
+
+
+
const handleLanguageChange = (langCode: string) => {
setLanguage(langCode as any);
};
diff --git a/packages/ui/src/components/UserAvatarBlock.tsx b/packages/ui/src/components/UserAvatarBlock.tsx
index d07f416c..b761fc1b 100644
--- a/packages/ui/src/components/UserAvatarBlock.tsx
+++ b/packages/ui/src/components/UserAvatarBlock.tsx
@@ -50,7 +50,13 @@ const UserAvatarBlock: React.FC
= ({
return;
}
e.stopPropagation();
- window.location.href = `/user/${userId}`;
+ const username = profile?.username;
+ if (username) {
+ navigate(`/user/${username}`);
+ } else {
+ console.warn("No username found for user", userId);
+ navigate(`/user/${userId}`); // Fallback
+ }
};
const nameClass = hoverStyle
diff --git a/packages/ui/src/components/feed/FeedCard.tsx b/packages/ui/src/components/feed/FeedCard.tsx
index 5f4add8d..63c3873e 100644
--- a/packages/ui/src/components/feed/FeedCard.tsx
+++ b/packages/ui/src/components/feed/FeedCard.tsx
@@ -24,17 +24,20 @@ export const FeedCard: React.FC = ({
onNavigate
}) => {
const navigate = useNavigate();
- const [isLiked, setIsLiked] = useState(false); // Need to hydrate this from props safely in real app
+ // Initialize from precomputed status (post.cover.is_liked or post.is_liked)
+ // We prioritize cover.is_liked if available, matching the server logic
+ const initialLiked = post.cover?.is_liked ?? post.is_liked ?? false;
+ const [isLiked, setIsLiked] = useState(initialLiked);
const [likeCount, setLikeCount] = useState(post.likes_count || 0);
const [lastTap, setLastTap] = useState(0);
const [showHeartAnimation, setShowHeartAnimation] = useState(false);
- // Initial check for like status (you might want to pass this in from parent if checking many)
- React.useEffect(() => {
- if (currentUserId && post.cover?.id) {
- db.checkLikeStatus(currentUserId, post.cover.id).then(setIsLiked);
- }
- }, [currentUserId, post.cover?.id]);
+ // Initial check removal: We now rely on server-provided `is_liked` status.
+ // React.useEffect(() => {
+ // if (currentUserId && post.cover?.id) {
+ // db.checkLikeStatus(currentUserId, post.cover.id).then(setIsLiked);
+ // }
+ // }, [currentUserId, post.cover?.id]);
const handleLike = async () => {
if (!currentUserId || !post.cover?.id) return;
@@ -79,7 +82,8 @@ export const FeedCard: React.FC = ({
if (item) {
const type = normalizeMediaType(item.type);
if (type === 'page-intern' && item.meta?.slug) {
- navigate(`/user/${item.user_id || post.user_id}/pages/${item.meta.slug}`);
+ const username = post.author?.username;
+ navigate(`/user/${username || item.user_id || post.user_id}/pages/${item.meta.slug}`);
return;
}
}
@@ -99,9 +103,9 @@ export const FeedCard: React.FC = ({
items={carouselItems}
aspectRatio={1}
className="w-full bg-muted"
- author={post.author_profile?.display_name || post.author_profile?.username || 'User'}
+ author={post.author?.display_name || post.author?.username || 'User'}
authorId={post.user_id}
- authorAvatarUrl={post.author_profile?.avatar_url}
+ authorAvatarUrl={post.author?.avatar_url}
onItemClick={handleItemClick}
/>
diff --git a/packages/ui/src/components/hmi/GenericCanvas.tsx b/packages/ui/src/components/hmi/GenericCanvas.tsx
index 8f612948..46ff4eff 100644
--- a/packages/ui/src/components/hmi/GenericCanvas.tsx
+++ b/packages/ui/src/components/hmi/GenericCanvas.tsx
@@ -15,6 +15,7 @@ interface GenericCanvasProps {
className?: string;
selectedWidgetId?: string | null;
onSelectWidget?: (widgetId: string) => void;
+ initialLayout?: any;
}
const GenericCanvasComponent: React.FC = ({
@@ -24,11 +25,13 @@ const GenericCanvasComponent: React.FC = ({
showControls = true,
className = '',
selectedWidgetId,
- onSelectWidget
+ onSelectWidget,
+ initialLayout
}) => {
const {
loadedPages,
loadPageLayout,
+ hydratePageLayout,
addWidgetToPage,
removeWidgetFromPage,
moveWidgetInPage,
@@ -45,11 +48,17 @@ const GenericCanvasComponent: React.FC = ({
const layout = loadedPages.get(pageId);
// Load the page layout on mount
+ // Load the page layout on mount or hydrate from prop
useEffect(() => {
+ if (initialLayout && !layout) {
+ hydratePageLayout(pageId, initialLayout);
+ return;
+ }
+
if (!layout) {
loadPageLayout(pageId, pageName);
}
- }, [pageId, pageName, layout, loadPageLayout]);
+ }, [pageId, pageName, layout, loadPageLayout, hydratePageLayout, initialLayout]);
const [selectedContainer, setSelectedContainer] = useState(null);
const [showWidgetPalette, setShowWidgetPalette] = useState(false);
diff --git a/packages/ui/src/components/widgets/MarkdownTextWidget.tsx b/packages/ui/src/components/widgets/MarkdownTextWidget.tsx
index 584fa8ac..03a4215f 100644
--- a/packages/ui/src/components/widgets/MarkdownTextWidget.tsx
+++ b/packages/ui/src/components/widgets/MarkdownTextWidget.tsx
@@ -31,6 +31,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { FilterPanel } from '@/components/filters/FilterPanel';
import AITextGenerator from '@/components/AITextGenerator';
import { getUserSecrets } from '@/components/ImageWizard/db';
+import * as db from '@/lib/db';
interface MarkdownTextWidgetProps {
isEditMode?: boolean;
@@ -85,24 +86,17 @@ const MarkdownTextWidget: React.FC = ({
// Load AI Text Generator settings from profile
useEffect(() => {
const loadSettings = async () => {
- if (!user) {
+ // Only load settings if user exists AND we are in edit mode
+ // This prevents 100s of requests when just viewing a page with many widgets
+ if (!user || !isEditMode) {
setSettingsLoaded(true);
return;
}
try {
- const { data: profile, error } = await supabase
- .from('profiles')
- .select('settings')
- .eq('user_id', user.id)
- .single();
-
- if (error) {
- setSettingsLoaded(true);
- return;
- }
-
- const settings = profile?.settings as any;
+ // Use the centralized cached fetcher instead of direct call
+ // This handles deduplication if multiple widgets load at once
+ const settings = await db.getUserSettings(user.id);
const aiTextSettings = settings?.aiTextGenerator;
if (aiTextSettings) {
@@ -125,7 +119,7 @@ const MarkdownTextWidget: React.FC = ({
};
loadSettings();
- }, [user]);
+ }, [user, isEditMode]);
// Save AI Text Generator settings to profile (debounced)
useEffect(() => {
@@ -133,13 +127,7 @@ const MarkdownTextWidget: React.FC = ({
const saveSettings = async () => {
try {
- const { data: profile } = await supabase
- .from('profiles')
- .select('settings')
- .eq('user_id', user.id)
- .single();
-
- const currentSettings = (profile?.settings as any) || {};
+ const currentSettings = await db.getUserSettings(user.id);
const updatedSettings = {
...currentSettings,
@@ -153,14 +141,7 @@ const MarkdownTextWidget: React.FC = ({
}
};
- const { error } = await supabase
- .from('profiles')
- .update({ settings: updatedSettings })
- .eq('user_id', user.id);
-
- if (error) {
- console.error('Failed to save AITextGenerator settings:', error);
- }
+ await db.updateUserSettings(user.id, updatedSettings);
} catch (error) {
console.error('Error saving AITextGenerator settings:', error);
}
@@ -242,15 +223,9 @@ const MarkdownTextWidget: React.FC = ({
}
try {
- const { data: userProvider, error } = await supabase
- .from('provider_configs')
- .select('settings')
- .eq('user_id', user.id)
- .eq('name', provider)
- .eq('is_active', true)
- .single();
+ const userProvider = await db.getProviderConfig(user.id, provider);
- if (error || !userProvider) {
+ if (!userProvider) {
// Only warn if checks in user_secrets also failed or if it's a different provider
if (provider !== 'openai') {
console.warn(`No provider configuration found for ${provider}`);
diff --git a/packages/ui/src/contexts/LayoutContext.tsx b/packages/ui/src/contexts/LayoutContext.tsx
index 53e29242..6b4daa95 100644
--- a/packages/ui/src/contexts/LayoutContext.tsx
+++ b/packages/ui/src/contexts/LayoutContext.tsx
@@ -21,6 +21,7 @@ interface LayoutContextType {
renameWidget: (pageId: string, widgetInstanceId: string, newId: string) => Promise;
exportPageLayout: (pageId: string) => Promise;
importPageLayout: (pageId: string, jsonData: string) => Promise;
+ hydratePageLayout: (pageId: string, layout: PageLayout) => void;
// Manual save
saveToApi: () => Promise;
@@ -383,6 +384,14 @@ export const LayoutProvider: React.FC = ({ children }) => {
}
};
+ const hydratePageLayout = (pageId: string, layout: PageLayout) => {
+ // Only set if not already loaded or if we want to force update (usually we want to trust the prop)
+ // But check timestamps? No, if we pass explicit data, we assume it's fresh.
+ if (!loadedPages.has(pageId)) {
+ setLoadedPages(prev => new Map(prev).set(pageId, layout));
+ }
+ };
+
const saveToApi = async (): Promise => {
try {
// Save all loaded pages to database
@@ -417,6 +426,7 @@ export const LayoutProvider: React.FC = ({ children }) => {
updateWidgetProps,
exportPageLayout,
importPageLayout,
+ hydratePageLayout,
saveToApi,
isLoading,
loadedPages,
diff --git a/packages/ui/src/contexts/ProfilesContext.tsx b/packages/ui/src/contexts/ProfilesContext.tsx
index a12ed6bc..533a72a9 100644
--- a/packages/ui/src/contexts/ProfilesContext.tsx
+++ b/packages/ui/src/contexts/ProfilesContext.tsx
@@ -1,5 +1,4 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
-import { supabase } from "@/integrations/supabase/client";
import { UserProfile } from "@/pages/Post/types";
interface ProfilesContextType {
@@ -62,7 +61,6 @@ export const ProfilesProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const fetchProfile = useCallback(async (userId: string) => {
if (profiles[userId]) return profiles[userId];
-
await fetchProfiles([userId]);
return profiles[userId] || null;
}, [profiles, fetchProfiles]);
diff --git a/packages/ui/src/hooks/useFeedData.ts b/packages/ui/src/hooks/useFeedData.ts
index d1234e9f..ba5714de 100644
--- a/packages/ui/src/hooks/useFeedData.ts
+++ b/packages/ui/src/hooks/useFeedData.ts
@@ -5,6 +5,8 @@ import { FEED_API_ENDPOINT, FEED_PAGE_SIZE } from '@/constants';
import { useProfiles } from '@/contexts/ProfilesContext';
import { useFeedCache } from '@/contexts/FeedCacheContext';
+const { supabase } = await import('@/integrations/supabase/client');
+
export type FeedSortOption = 'latest' | 'top';
interface UseFeedDataProps {
@@ -118,30 +120,54 @@ export const useFeedData = ({
console.log('Hydrated feed', fetchedPosts);
}
- // 2. API Fetch (Home only)
- else if (source === 'home' && !sourceId) {
+ // 2. API Fetch (Universal)
+ // Prioritize API if endpoint exists. Using API allows server-side handling of complicated logic.
+ // Client still falls back to DB if API fails? Or we just error.
+ // Let's use API as primary.
+ if (true) {
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
- const fetchUrl = SERVER_URL
- ? `${SERVER_URL}${FEED_API_ENDPOINT}?page=${currentPage}&limit=${FEED_PAGE_SIZE}`
- : `${FEED_API_ENDPOINT}?page=${currentPage}&limit=${FEED_PAGE_SIZE}`;
+ let queryParams = `?page=${currentPage}&limit=${FEED_PAGE_SIZE}&sortBy=${sortBy}`;
+ if (source) queryParams += `&source=${source}`;
+ if (sourceId) queryParams += `&sourceId=${sourceId}`;
- const res = await fetch(fetchUrl);
- if (!res.ok) throw new Error(`Feed fetch failed: ${res.statusText}`);
- fetchedPosts = await res.json();
+ // If we have token, pass it?
+ // The Supabase client in the hook prop (supabaseClient) or defaultSupabase usually has the session.
+ // We should pass the Authorization header.
+ // We can get the session from the client.
+ const client = supabaseClient || supabase;
+
+ const { data: { session } } = await client.auth.getSession();
+ const headers: Record = {};
+ if (session?.access_token) {
+ headers['Authorization'] = `Bearer ${session.access_token}`;
+ }
+
+ const fetchUrl = SERVER_URL
+ ? `${SERVER_URL}${FEED_API_ENDPOINT}${queryParams}`
+ : `${FEED_API_ENDPOINT}${queryParams}`;
+
+ const res = await fetch(fetchUrl, { headers });
+ if (!res.ok) {
+ // Fallback to DB if API fails (e.g. offline)?
+ console.warn('Feed API failed, falling back to direct DB', res.statusText);
+ // Allow falling through to step 3?
+ // If 404/500, maybe.
+ // IMPORTANT: If we want to strictly use API, we should throw.
+ // But user said "most of the client db queries".
+ // Let's try to fallback to DB if API fails, for robustness during migration.
+ throw new Error(`Feed fetch failed: ${res.statusText}`);
+ } else {
+ fetchedPosts = await res.json();
+ }
}
- // 3. Fallback DB Fetch
+ // 3. Fallback DB Fetch (Disabled if API succeeded, or if we caught error and want to fallback)
+ // Logic above throws on error, so we won't reach here if API succeeds.
+ // If we want fallback, we should try-catch inside.
+ /*
else {
- console.log('Fetching feed from DB', source, sourceId, isOrgContext, orgSlug, currentPage);
- fetchedPosts = await db.fetchFeedPostsPaginated(
- source,
- sourceId,
- isOrgContext,
- orgSlug,
- currentPage,
- FEED_PAGE_SIZE,
- supabaseClient
- );
- }
+ // ...
+ }
+ */
if (fetchedPosts.length < FEED_PAGE_SIZE) {
setHasMore(false);
diff --git a/packages/ui/src/hooks/usePromptHistory.ts b/packages/ui/src/hooks/usePromptHistory.ts
index 271ffbc7..8536c18d 100644
--- a/packages/ui/src/hooks/usePromptHistory.ts
+++ b/packages/ui/src/hooks/usePromptHistory.ts
@@ -4,6 +4,7 @@ import { supabase } from '@/integrations/supabase/client';
import { useLog } from '@/contexts/LogContext';
import { toast } from 'sonner';
import { translate } from '@/i18n';
+import { getUserSettings, updateUserSettings } from '@/lib/db';
const MAX_HISTORY_LENGTH = 50;
@@ -18,17 +19,9 @@ export const usePromptHistory = () => {
const fetchHistory = async () => {
if (user) {
try {
- const { data, error } = await supabase
- .from('profiles')
- .select('settings')
- .eq('user_id', user.id)
- .single();
-
- if (error) throw error;
-
- const settings = data?.settings;
+ const settings = await getUserSettings(user.id);
const history = (settings && typeof settings === 'object' && 'promptHistory' in settings && Array.isArray(settings.promptHistory))
- ? settings.promptHistory.filter((item): item is string => typeof item === 'string')
+ ? settings.promptHistory.filter((item: any): item is string => typeof item === 'string')
: [];
setPromptHistory(history);
} catch (error: any) {
@@ -43,26 +36,13 @@ export const usePromptHistory = () => {
if (user) {
try {
// First, fetch the current settings to avoid overwriting them
- const { data: profileData, error: fetchError } = await supabase
- .from('profiles')
- .select('settings')
- .eq('user_id', user.id)
- .single();
-
- if (fetchError) throw fetchError;
-
- const currentSettings = (profileData?.settings && typeof profileData.settings === 'object') ? profileData.settings : {};
+ const currentSettings = await getUserSettings(user.id);
const newSettings = {
...currentSettings,
promptHistory: newHistory,
};
- const { error } = await supabase
- .from('profiles')
- .update({ settings: newSettings })
- .eq('user_id', user.id);
-
- if (error) throw error;
+ await updateUserSettings(user.id, newSettings);
addLog('debug', '[PromptHistory] Saved updated prompt history to Supabase');
} catch (error: any) {
addLog('error', '[PromptHistory] Failed to save history', error);
diff --git a/packages/ui/src/image-api.ts b/packages/ui/src/image-api.ts
index d2c04241..0680f546 100644
--- a/packages/ui/src/image-api.ts
+++ b/packages/ui/src/image-api.ts
@@ -35,6 +35,8 @@ interface ImageResult {
text?: string;
}
+import { getUserGoogleApiKey } from '@/lib/db';
+
// Get user's Google API key from user_secrets
export const getGoogleApiKey = async (): Promise => {
try {
@@ -44,19 +46,7 @@ export const getGoogleApiKey = async (): Promise => {
return null;
}
- const { data: secretData, error } = await supabase
- .from('user_secrets')
- .select('settings')
- .eq('user_id', user.id)
- .maybeSingle();
-
- if (error) {
- logger.error('Error fetching user secrets:', error);
- return null;
- }
-
- const settings = secretData?.settings as { api_keys?: Record } | null;
- const apiKey = settings?.api_keys?.google_api_key;
+ const apiKey = await getUserGoogleApiKey(user.id);
if (!apiKey) {
logger.error('No Google API key found in user secrets. Please add your Google API key in your profile settings.');
diff --git a/packages/ui/src/lib/db.ts b/packages/ui/src/lib/db.ts
index 87a926b8..7b1f890f 100644
--- a/packages/ui/src/lib/db.ts
+++ b/packages/ui/src/lib/db.ts
@@ -3,24 +3,105 @@ import { UserProfile, PostMediaItem } from "@/pages/Post/types";
import { MediaType, MediaItem } from "@/types";
import { SupabaseClient } from "@supabase/supabase-js";
-// Request cache for deduplication
+export interface FeedPost {
+ id: string; // Post ID
+ title: string;
+ description: string | null;
+ created_at: string;
+ user_id: string;
+ pictures: MediaItem[]; // All visible pictures
+ cover: MediaItem; // The selected cover picture
+ likes_count: number;
+ comments_count: number;
+ type: MediaType;
+ author?: UserProfile;
+ settings?: any;
+ is_liked?: boolean;
+}
+
const requestCache = new Map>();
-const fetchWithDeduplication = async (key: string, fetcher: () => Promise): Promise => {
+type CacheStorageType = 'memory' | 'local';
+
+interface StoredCacheItem {
+ value: T;
+ timestamp: number;
+ timeout: number;
+}
+
+const fetchWithDeduplication = async (
+ key: string,
+ fetcher: () => Promise,
+ timeout: number = 25000,
+ storage: CacheStorageType = 'local'
+): Promise => {
+ // 1. Check LocalStorage if requested
+ if (storage === 'local' && typeof window !== 'undefined') {
+ const localKey = `db-cache-${key}`;
+ const stored = localStorage.getItem(localKey);
+ if (stored) {
+ try {
+ const item: StoredCacheItem = JSON.parse(stored);
+ if (Date.now() - item.timestamp < item.timeout) {
+ console.debug(`[db] Local Cache HIT: ${key}`);
+ return item.value;
+ } else {
+ localStorage.removeItem(localKey); // Clean up expired
+ }
+ } catch (e) {
+ console.warn('Failed to parse persistent cache item', e);
+ localStorage.removeItem(localKey);
+ }
+ }
+ }
+
+ // 2. Check Memory Cache (In-flight or recent)
if (!requestCache.has(key)) {
- const promise = fetcher().catch((err) => {
+ console.info(`[db] Cache MISS: ${key}`);
+ const promise = fetcher().then((data) => {
+ // Save to LocalStorage if requested and successful
+ if (storage === 'local' && typeof window !== 'undefined') {
+ const localKey = `db-cache-${key}`;
+ const item: StoredCacheItem = {
+ value: data,
+ timestamp: Date.now(),
+ timeout: timeout
+ };
+ try {
+ localStorage.setItem(localKey, JSON.stringify(item));
+ } catch (e) {
+ console.warn('Failed to save to persistent cache', e);
+ }
+ }
+ return data;
+ }).catch((err) => {
requestCache.delete(key);
throw err;
}).finally(() => {
- // Clear cache after a short delay to allow immediate re-renders to share data
- // but prevent stale data issues.
- setTimeout(() => requestCache.delete(key), 2500);
+ // Clear memory cache after timeout to allow new fetches
+ // For 'local' storage, we technically might not need to clear memory cache as fast,
+ // but keeping them in sync involves less complexity if we just let memory cache expire.
+ // If it expires from memory, next call checks local storage again.
+ timeout && setTimeout(() => requestCache.delete(key), timeout);
});
requestCache.set(key, promise);
+ } else {
+ console.debug(`[db] Cache HIT: ${key}`);
}
+
return requestCache.get(key) as Promise;
};
+export const invalidateCache = (key: string) => {
+ // Clear memory cache
+ requestCache.delete(key);
+
+ // Clear local storage cache
+ if (typeof window !== 'undefined') {
+ localStorage.removeItem(`db-cache-${key}`);
+ }
+};
+
export const fetchPostById = async (id: string, client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`post-${id}`, async () => {
@@ -218,6 +299,7 @@ export const upsertPictures = async (pictures: Partial[], client?
export const getUserSettings = async (userId: string, client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
+ console.log('getUserSettings', userId);
return fetchWithDeduplication(`settings-${userId}`, async () => {
const { data, error } = await supabase
.from('profiles')
@@ -226,7 +308,7 @@ export const getUserSettings = async (userId: string, client?: SupabaseClient) =
.single();
if (error) throw error;
return (data?.settings as any) || {};
- });
+ }, 100000);
};
export const updateUserSettings = async (userId: string, settings: any, client?: SupabaseClient) => {
@@ -256,6 +338,75 @@ export const getUserOpenAIKey = async (userId: string, client?: SupabaseClient)
});
}
+export const getUserGoogleApiKey = async (userId: string, client?: SupabaseClient) => {
+ const supabase = client || defaultSupabase;
+ return fetchWithDeduplication(`google-${userId}`, async () => {
+ const { data, error } = await supabase
+ .from('user_secrets')
+ .select('settings')
+ .eq('user_id', userId)
+ .maybeSingle();
+
+ if (error) throw error;
+ const settings = data?.settings as any;
+ return settings?.api_keys?.google_api_key;
+ });
+}
+
+export const getProviderConfig = async (userId: string, provider: string, client?: SupabaseClient) => {
+ const supabase = client || defaultSupabase;
+ return fetchWithDeduplication(`provider-${userId}-${provider}`, async () => {
+ const { data, error } = await supabase
+ .from('provider_configs')
+ .select('settings')
+ .eq('user_id', userId)
+ .eq('name', provider)
+ .eq('is_active', true)
+ .single();
+
+ if (error) {
+ // It's common to not have configs for all providers, so we might want to suppress some errors or handle them gracefully
+ // However, checking error code might be robust. For now let's just throw if it's not a "no rows" error,
+ // or just return null if not found.
+ // The original code used .single() which errors if 0 rows.
+ // Let's use maybeSingle() to be safe? The original code caught the error and returned null.
+ // But the original query strictly used .single().
+ // Let's stick to .single() but catch it here if we want to mimic exact behavior, OR use maybeSingle and return null.
+ // The calling code expects null if not found.
+ if (error.code === 'PGRST116') return null; // No rows found
+ throw error;
+ }
+ return data as { settings: any };
+ });
+}
+
+export const fetchUserPage = async (userId: string, slug: string, client?: SupabaseClient) => {
+ const supabase = client || defaultSupabase;
+ const key = `user-page-${userId}-${slug}`;
+ // Cache for 10 minutes (600000ms)
+ return fetchWithDeduplication(key, async () => {
+ const { data: sessionData } = await supabase.auth.getSession();
+ const token = sessionData.session?.access_token;
+
+ const headers: HeadersInit = {};
+ if (token) headers['Authorization'] = `Bearer ${token}`;
+
+ const res = await fetch(`/api/user-page/${userId}/${slug}`, { headers });
+
+ if (!res.ok) {
+ if (res.status === 404) return null;
+ throw new Error(`Failed to fetch user page: ${res.statusText}`);
+ }
+
+ return await res.json();
+ }, 600000);
+};
+
+export const invalidateUserPageCache = (userId: string, slug: string) => {
+ const key = `user-page-${userId}-${slug}`;
+ invalidateCache(key);
+};
+
export const addCollectionPictures = async (inserts: { collection_id: string, picture_id: string }[], client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
const { error } = await supabase
@@ -354,20 +505,6 @@ export const fetchUserLikesForPictures = async (userId: string, pictureIds: stri
});
};
-export interface FeedPost {
- id: string; // Post ID
- title: string;
- description: string | null;
- created_at: string;
- user_id: string;
- pictures: MediaItem[]; // All visible pictures
- cover: MediaItem; // The selected cover picture
- likes_count: number;
- comments_count: number;
- type: MediaType;
- author_profile?: UserProfile;
-}
-
export const fetchFeedPosts = async (
source: 'home' | 'collection' | 'tag' | 'user' | 'widget' = 'home',
sourceId?: string,
@@ -375,7 +512,6 @@ export const fetchFeedPosts = async (
orgSlug?: string,
client?: SupabaseClient
): Promise => {
- // Forward to paginated version with defaults
return fetchFeedPostsPaginated(source, sourceId, isOrgContext, orgSlug, 0, 30, client);
};
@@ -435,6 +571,25 @@ export const fetchFeedPostsPaginated = async (
const { data: postsData, error: postsError } = await query;
if (postsError) throw postsError;
+ // Manually fetch profiles since foreign key might be missing
+ if (postsData && postsData.length > 0) {
+ const userIds = Array.from(new Set(postsData.map((p: any) => p.user_id).filter(Boolean)));
+ console.log('userIds', userIds);
+ if (userIds.length > 0) {
+ const { data: profiles } = await supabase
+ .from('profiles')
+ .select('user_id, username, display_name, avatar_url')
+ .in('user_id', userIds);
+
+ if (profiles) {
+ const profileMap = new Map(profiles.map(p => [p.user_id, p]));
+ postsData.forEach((p: any) => {
+ p.author = profileMap.get(p.user_id);
+ });
+ }
+ }
+ }
+
// 2. Fetch Pages (if applicable)
// Only fetch pages for home/user/org sources to keep it simple for now
let pagesData: any[] = [];
@@ -683,6 +838,9 @@ export const mapFeedPostsToMediaItems = (posts: FeedPost[], sortBy: 'latest' | '
}
if (!cover) return null;
+
+ const versionCount = post.pictures ? post.pictures.length : 1;
+
return {
id: post.id,
picture_id: cover.id,
@@ -698,12 +856,13 @@ export const mapFeedPostsToMediaItems = (posts: FeedPost[], sortBy: 'latest' | '
comments: [{ count: post.comments_count }],
responsive: (cover as any).responsive,
job: (cover as any).job,
- // author_profile must be populated externally
+ author: post.author,
+ versionCount
};
}).filter(item => item !== null);
};
-// Augment posts if they come from API/Hydration (missing cover/author_profile)
+// Augment posts if they come from API/Hydration (missing cover/author)
export const augmentFeedPosts = (posts: any[]): FeedPost[] => {
return posts.map(p => {
// Check if we need to augment (heuristic: missing cover)
@@ -715,12 +874,12 @@ export const augmentFeedPosts = (posts: any[]): FeedPost[] => {
return {
...p,
cover: validPics[0] || pics[0], // fallback to first if none visible?
- author_profile: p.author ? {
+ author: p.author || (p.author ? {
user_id: p.author.user_id,
username: p.author.username,
display_name: p.author.display_name,
avatar_url: p.author.avatar_url
- } : undefined
+ } : undefined)
};
}
return p;
diff --git a/packages/ui/src/lib/openai.ts b/packages/ui/src/lib/openai.ts
index bd6f81d0..7fd5affe 100644
--- a/packages/ui/src/lib/openai.ts
+++ b/packages/ui/src/lib/openai.ts
@@ -18,6 +18,7 @@ import { JSONSchema } from 'openai/lib/jsonschema';
import { createImage as createImageRouter, editImage as editImageRouter } from '@/lib/image-router';
import { generateTextWithImagesTool } from '@/lib/markdownImageTools';
import { createPageTool } from '@/lib/pageTools';
+import { getUserOpenAIKey } from '@/lib/db';
type LogFunction = (level: string, message: string, data?: any) => void;
@@ -80,19 +81,7 @@ const getOpenAIApiKey = async (): Promise => {
return null;
}
- const { data: secretData, error } = await supabase
- .from('user_secrets')
- .select('settings')
- .eq('user_id', user.id)
- .maybeSingle();
-
- if (error) {
- consoleLogger.error('Error fetching user secrets:', error);
- return null;
- }
-
- const settings = secretData?.settings as { api_keys?: Record } | null;
- const apiKey = settings?.api_keys?.openai_api_key;
+ const apiKey = await getUserOpenAIKey(user.id);
if (!apiKey) {
consoleLogger.error('No OpenAI API key found in user secrets. Please add your OpenAI API key in your profile settings.');
diff --git a/packages/ui/src/lib/toc.ts b/packages/ui/src/lib/toc.ts
index 36e0570e..c11070cb 100644
--- a/packages/ui/src/lib/toc.ts
+++ b/packages/ui/src/lib/toc.ts
@@ -68,7 +68,7 @@ export function extractHeadingsFromLayout(layout: PageLayout): MarkdownHeading[]
container.children.forEach(processContainer);
};
- layout.containers.forEach(processContainer);
+ layout.containers?.forEach(processContainer);
return allHeadings;
}
diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx
index ea05e58b..332496d2 100644
--- a/packages/ui/src/main.tsx
+++ b/packages/ui/src/main.tsx
@@ -1,16 +1,5 @@
import { createRoot } from "react-dom/client";
-import { registerSW } from 'virtual:pwa-register'
import App from "./App.tsx";
-
-// Register Service Worker
-const updateSW = registerSW({
- onNeedRefresh() {
- // Optionally ask user to refresh, but for now we auto-update
- },
- onOfflineReady() {
- console.log('PWA App is ready to work offline')
- },
-})
import "./index.css";
import { ThemeProvider } from "@/components/ThemeProvider";
diff --git a/packages/ui/src/pages/PlaygroundEditorLLM.tsx b/packages/ui/src/pages/PlaygroundEditorLLM.tsx
index 4db52e39..864362e2 100644
--- a/packages/ui/src/pages/PlaygroundEditorLLM.tsx
+++ b/packages/ui/src/pages/PlaygroundEditorLLM.tsx
@@ -16,6 +16,8 @@ import { toast } from 'sonner';
import { translate } from '@/i18n';
import OpenAI from 'openai';
import SimpleLogViewer from '@/components/SimpleLogViewer';
+import { getUserSettings, updateUserSettings, getProviderConfig } from '@/lib/db';
+import { usePromptHistory } from '@/hooks/usePromptHistory';
const PlaygroundEditorLLM: React.FC = () => {
const { user } = useAuth();
@@ -23,16 +25,26 @@ const PlaygroundEditorLLM: React.FC = () => {
const [selectedText, setSelectedText] = useState('');
// AI Text Generator state
- const [prompt, setPrompt] = useState('');
- const [promptHistory, setPromptHistory] = useState([]);
- const [historyIndex, setHistoryIndex] = useState(-1);
+ // AI Text Generator state
+ const {
+ prompt,
+ setPrompt,
+ promptHistory,
+ historyIndex,
+ setHistoryIndex,
+ navigateHistory,
+ addPromptToHistory
+ } = usePromptHistory();
+
const [selectedProvider, setSelectedProvider] = useState('openai');
const [selectedModel, setSelectedModel] = useState('gpt-5');
const [imageToolsEnabled, setImageToolsEnabled] = useState(false);
+ const [webSearchEnabled, setWebSearchEnabled] = useState(false);
const [contextMode, setContextMode] = useState<'clear' | 'selection' | 'all'>('all');
const [applicationMode, setApplicationMode] = useState<'replace' | 'insert' | 'append'>('append');
const [templates, setTemplates] = useState>([]);
const [streamMode, setStreamMode] = useState(false);
+ const [settingsLoaded, setSettingsLoaded] = useState(false);
// Generation state
const [isGenerating, setIsGenerating] = useState(false);
@@ -48,6 +60,70 @@ const PlaygroundEditorLLM: React.FC = () => {
const lastSelectionRef = useRef<{ isAtStart: boolean } | null>(null);
const insertTransactionRef = useRef<{ text: string, onComplete: () => void } | null>(null);
+ // Load settings
+ useEffect(() => {
+ const loadSettings = async () => {
+ if (!user) {
+ setSettingsLoaded(true);
+ return;
+ }
+
+ try {
+ const settings = await getUserSettings(user.id);
+ const aiTextSettings = settings?.aiTextGenerator;
+ const savedTemplates = settings?.promptTemplates || [];
+
+ if (aiTextSettings) {
+ if (aiTextSettings.provider) setSelectedProvider(aiTextSettings.provider);
+ if (aiTextSettings.model) setSelectedModel(aiTextSettings.model);
+ if (typeof aiTextSettings.imageToolsEnabled === 'boolean') setImageToolsEnabled(aiTextSettings.imageToolsEnabled);
+ if (aiTextSettings.contextMode) setContextMode(aiTextSettings.contextMode);
+ if (aiTextSettings.applicationMode) setApplicationMode(aiTextSettings.applicationMode);
+ }
+
+ if (savedTemplates.length > 0) {
+ setTemplates(savedTemplates);
+ }
+ } catch (error) {
+ console.error('Error loading settings:', error);
+ } finally {
+ setSettingsLoaded(true);
+ }
+ };
+
+ loadSettings();
+ }, [user]);
+
+ // Save settings (debounced)
+ useEffect(() => {
+ if (!user || !settingsLoaded) return;
+
+ const saveSettings = async () => {
+ try {
+ const currentSettings = await getUserSettings(user.id);
+
+ const updatedSettings = {
+ ...currentSettings,
+ aiTextGenerator: {
+ provider: selectedProvider,
+ model: selectedModel,
+ imageToolsEnabled,
+ // webSearchEnabled: false, // Not locally managed yet
+ contextMode,
+ applicationMode,
+ }
+ };
+
+ await updateUserSettings(user.id, updatedSettings);
+ } catch (error) {
+ console.error('Error saving settings:', error);
+ }
+ };
+
+ const timeoutId = setTimeout(saveSettings, 1000);
+ return () => clearTimeout(timeoutId);
+ }, [user, settingsLoaded, selectedProvider, selectedModel, imageToolsEnabled, contextMode, applicationMode]);
+
const handleContentChange = useCallback((newContent: string) => {
setContent(newContent);
@@ -81,29 +157,7 @@ const PlaygroundEditorLLM: React.FC = () => {
}, []);
- const addToHistory = useCallback((text: string) => {
- if (text.trim()) {
- setPromptHistory(prev => [text, ...prev.slice(0, 49)]);
- setHistoryIndex(-1);
- }
- }, []);
-
- const navigateHistory = useCallback((direction: 'up' | 'down') => {
- if (direction === 'up' && historyIndex < promptHistory.length - 1) {
- const newIndex = historyIndex + 1;
- setHistoryIndex(newIndex);
- setPrompt(promptHistory[newIndex]);
- } else if (direction === 'down') {
- if (historyIndex > 0) {
- const newIndex = historyIndex - 1;
- setHistoryIndex(newIndex);
- setPrompt(promptHistory[newIndex]);
- } else {
- setHistoryIndex(-1);
- setPrompt('');
- }
- }
- }, [historyIndex, promptHistory]);
+ // History handlers replaced by hook
const getProviderApiKey = useCallback(async (provider: string): Promise => {
if (!user) return null;
@@ -121,15 +175,9 @@ const PlaygroundEditorLLM: React.FC = () => {
}
try {
- const { data: userProvider, error } = await supabase
- .from('provider_configs')
- .select('settings')
- .eq('user_id', user.id)
- .eq('name', provider)
- .eq('is_active', true)
- .single();
+ const userProvider = await getProviderConfig(user.id, provider);
- if (error || !userProvider) {
+ if (!userProvider) {
if (provider !== 'openai') {
// console.warn(\`No provider configuration found for \${provider}\`);
}
@@ -240,7 +288,7 @@ const PlaygroundEditorLLM: React.FC = () => {
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
setIsGenerating(true);
- addToHistory(prompt);
+ addPromptToHistory(prompt);
try {
// Build context prompt
@@ -384,7 +432,7 @@ const PlaygroundEditorLLM: React.FC = () => {
}
}, [
prompt, user, applicationMode, contextMode, selectedText, content, imageToolsEnabled,
- selectedProvider, selectedModel, addToHistory, generateWithProvider, getProviderApiKey, streamMode
+ selectedProvider, selectedModel, addPromptToHistory, generateWithProvider, getProviderApiKey, streamMode
]);
const handleOptimize = useCallback(async () => {
@@ -472,7 +520,7 @@ const PlaygroundEditorLLM: React.FC = () => {
}
}, [isRecording]);
- const handleSaveTemplate = useCallback(() => {
+ const handleSaveTemplate = useCallback(async () => {
if (!prompt.trim()) {
toast.error('Please enter a prompt to save');
return;
@@ -480,15 +528,43 @@ const PlaygroundEditorLLM: React.FC = () => {
const name = window.prompt('Enter template name:');
if (name) {
- setTemplates(prev => [...prev, { name, template: prompt }]);
+ const newTemplates = [...templates, { name, template: prompt }];
+ setTemplates(newTemplates);
toast.success('Template saved!');
- }
- }, [prompt]);
- const handleDeleteTemplate = useCallback((index: number) => {
- setTemplates(prev => prev.filter((_, i) => i !== index));
+ // Save to server
+ if (user) {
+ try {
+ const currentSettings = await getUserSettings(user.id);
+ await updateUserSettings(user.id, {
+ ...currentSettings,
+ promptTemplates: newTemplates
+ });
+ } catch (err) {
+ console.error('Error saving templates', err);
+ }
+ }
+ }
+ }, [prompt, templates, user]);
+
+ const handleDeleteTemplate = useCallback(async (index: number) => {
+ const newTemplates = templates.filter((_, i) => i !== index);
+ setTemplates(newTemplates);
toast.success('Template deleted');
- }, []);
+
+ // Save to server
+ if (user) {
+ try {
+ const currentSettings = await getUserSettings(user.id);
+ await updateUserSettings(user.id, {
+ ...currentSettings,
+ promptTemplates: newTemplates
+ });
+ } catch (err) {
+ console.error('Error saving templates', err);
+ }
+ }
+ }, [templates, user]);
const handleApplyTemplate = useCallback((template: string) => {
setPrompt(template);
@@ -539,6 +615,8 @@ const PlaygroundEditorLLM: React.FC = () => {
onModelChange={setSelectedModel}
imageToolsEnabled={imageToolsEnabled}
onImageToolsChange={setImageToolsEnabled}
+ webSearchEnabled={webSearchEnabled}
+ onWebSearchChange={setWebSearchEnabled}
contextMode={contextMode}
onContextModeChange={setContextMode}
hasSelection={!!selectedText && selectedText.trim().length > 0}
diff --git a/packages/ui/src/pages/Post.tsx b/packages/ui/src/pages/Post.tsx
index 8fe3a09b..c44fde3e 100644
--- a/packages/ui/src/pages/Post.tsx
+++ b/packages/ui/src/pages/Post.tsx
@@ -18,6 +18,7 @@ import { CompactRenderer } from "./Post/renderers/CompactRenderer";
import { usePostActions } from "./Post/usePostActions";
import { exportMarkdown, downloadMediaItem } from "./Post/PostActions";
import { DeleteDialog } from "./Post/components/DeleteDialogs";
+
import '@vidstack/react/player/styles/default/theme.css';
import '@vidstack/react/player/styles/default/layouts/video.css';
@@ -426,9 +427,17 @@ const Post = ({ postId: propPostId, embedded = false, className }: PostProps) =>
useEffect(() => {
if (mediaItem) {
- loadVersions();
- fetchAuthorProfile();
- checkIfLiked(mediaItem.id);
+ // loadVersions(); // Deprecated: Versions handled by server aggregation
+ // fetchAuthorProfile(); // Deprecated: Author returned in post details
+ // checkIfLiked(mediaItem.id); // Deprecated: is_liked returned in post details
+
+ // We still update local like state when mediaItem changes
+ if (mediaItem.is_liked !== undefined) {
+ setIsLiked(mediaItem.is_liked || false);
+ }
+ if (mediaItem.likes_count !== undefined) {
+ setLikesCount(mediaItem.likes_count);
+ }
}
}, [mediaItem, user]);
diff --git a/packages/ui/src/pages/Post/db.ts b/packages/ui/src/pages/Post/db.ts
index 4521e55a..82def225 100644
--- a/packages/ui/src/pages/Post/db.ts
+++ b/packages/ui/src/pages/Post/db.ts
@@ -1 +1,33 @@
export * from '@/lib/db';
+
+export const fetchPostDetailsAPI = async (id: string, options: { sizes?: string, formats?: string } = {}) => {
+ const params = new URLSearchParams();
+ if (options.sizes) params.set('sizes', options.sizes);
+ if (options.formats) params.set('formats', options.formats);
+
+ const qs = params.toString();
+ const url = `/api/posts/${id}${qs ? `?${qs}` : ''}`;
+
+ // We rely on the browser/hook to handle auth headers if global fetch is intercepted,
+ // OR we explicitly get session?
+ // Usually standard `fetch` in our app might not send auth if using implicit flows or we need to pass headers.
+ // In `useFeedData`, we manually added headers.
+ // Let's assume we need to handle auth here or use a helper that does.
+ // To keep it simple for now, we'll import `supabase` and get session.
+
+ const { supabase } = await import('@/integrations/supabase/client');
+ const { data: { session } } = await supabase.auth.getSession();
+
+ const headers: Record = {};
+ if (session?.access_token) {
+ headers['Authorization'] = `Bearer ${session.access_token}`;
+ }
+
+ const res = await fetch(url, { headers });
+ if (!res.ok) {
+ if (res.status === 404) return null;
+ throw new Error(`Failed to fetch post: ${res.statusText}`);
+ }
+
+ return res.json();
+};
diff --git a/packages/ui/src/pages/UserPage.tsx b/packages/ui/src/pages/UserPage.tsx
index 54bab8aa..011d429c 100644
--- a/packages/ui/src/pages/UserPage.tsx
+++ b/packages/ui/src/pages/UserPage.tsx
@@ -6,7 +6,6 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ArrowLeft, FileText, Calendar, Eye, EyeOff, Edit, Edit3, Check, X, Plus, PanelLeftClose, PanelLeftOpen } from "lucide-react";
-import { ThemeToggle } from "@/components/ThemeToggle";
import { T, translate } from "@/i18n";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
@@ -19,6 +18,7 @@ import { TableOfContents } from "@/components/sidebar/TableOfContents";
import { MobileTOC } from "@/components/sidebar/MobileTOC";
import { extractHeadings, extractHeadingsFromLayout, MarkdownHeading } from "@/lib/toc";
import { useLayout } from "@/contexts/LayoutContext";
+import { fetchUserPage, invalidateUserPageCache } from "@/lib/db";
interface Page {
id: string;
@@ -54,12 +54,15 @@ interface UserPageProps {
}
const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initialPage }: UserPageProps) => {
- const { userId: paramUserId, slug: paramSlug, orgSlug } = useParams<{ userId: string; slug: string; orgSlug?: string }>();
+ const { userId: paramUserId, username: paramUsername, slug: paramSlug, orgSlug } = useParams<{ userId: string; username: string; slug: string; orgSlug?: string }>();
const navigate = useNavigate();
const { user: currentUser } = useAuth();
const { getLoadedPageLayout, loadPageLayout } = useLayout();
- const userId = propUserId || paramUserId;
+ const [resolvedUserId, setResolvedUserId] = useState(null);
+
+ // Determine effective userId - either from prop, existing param, or resolved from username
+ const userId = propUserId || paramUserId || resolvedUserId;
const slug = propSlug || paramSlug;
const [page, setPage] = useState(initialPage || null);
@@ -90,94 +93,45 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
setLoading(false);
return;
}
- if (userId && slug) {
- fetchPage();
- fetchUserProfile();
+
+ // Determine effective identifier
+ const identifier = propUserId || paramUserId || paramUsername;
+ const effectiveSlug = propSlug || paramSlug;
+
+ if (identifier && effectiveSlug) {
+ fetchUserPageData(identifier, effectiveSlug);
}
- }, [userId, slug, initialPage]);
+ }, [propUserId, paramUserId, paramUsername, propSlug, paramSlug, initialPage]);
- const fetchChildPages = async (parentId: string) => {
+ const fetchUserPageData = async (id: string, slugStr: string) => {
+ setLoading(true);
try {
- let query = supabase
- .from('pages')
- .select('id, title, slug, visible, is_public')
- .eq('parent', parentId)
- .order('title');
+ const data = await fetchUserPage(id, slugStr);
- if (!isOwner) {
- query = query.eq('visible', true).eq('is_public', true);
- }
+ if (data) {
+ setPage(data.page);
+ setUserProfile(data.userProfile as any); // Cast to match local interface if needed
+ setChildPages(data.childPages || []);
- const { data, error } = await query;
-
- if (error) throw error;
- setChildPages(data || []);
- } catch (error) {
- console.error('Error fetching child pages:', error);
- }
- };
-
- const fetchPage = async () => {
- try {
- setLoading(true);
-
- let query = supabase
- .from('pages')
- .select('*')
- .eq('slug', slug)
- .eq('owner', userId);
-
- // If not owner, only show public and visible pages
- if (!isOwner) {
- query = query.eq('is_public', true).eq('visible', true);
- }
-
- const { data, error } = await query.maybeSingle();
-
- if (error) throw error;
-
- if (!data) {
+ // If we resolved via username, ensure we have the userId for isOwner check
+ if (!resolvedUserId && data.page.owner) {
+ setResolvedUserId(data.page.owner);
+ }
+ } else {
toast.error(translate('Page not found or you do not have access'));
- navigate(orgSlug ? `/org/${orgSlug}/user/${userId}` : `/user/${userId}`);
- return;
+ navigate(orgSlug ? `/org/${orgSlug}/user/${id}` : `/user/${id}`);
}
-
- setPage(data as Page);
-
- // Fetch parent page if it exists
- if (data.parent) {
- fetchParentPage(data.parent);
- }
-
- // Fetch child pages
- fetchChildPages(data.id);
} catch (error) {
- console.error('Error fetching page:', error);
+ console.error('Error fetching user page:', error);
toast.error(translate('Failed to load page'));
- navigate(orgSlug ? `/org/${orgSlug}/user/${userId}` : `/user/${userId}`);
} finally {
setLoading(false);
}
};
- const fetchParentPage = async (parentId: string) => {
- try {
- const { data, error } = await supabase
- .from('pages')
- .select('title, slug')
- .eq('id', parentId)
- .single();
- if (error) throw error;
- setPage(prev => prev ? ({
- ...prev,
- parent_page: data
- }) : null);
- } catch (error) {
- console.error('Error fetching parent page:', error);
- }
- };
+
// Reactive Heading Extraction
// This ensures we extract headings whenever the page loads OR when specific layouts are loaded into context
@@ -226,43 +180,20 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
}
}, [headings]); // Run when headings are populated
- const fetchUserProfile = async () => {
- try {
- const { data: profile } = await supabase
- .from('profiles')
- .select('user_id, username, display_name, avatar_url')
- .eq('user_id', userId)
- .maybeSingle();
- if (profile) {
- setUserProfile({
- id: profile.user_id,
- username: profile.username,
- display_name: profile.display_name,
- avatar_url: profile.avatar_url,
- });
- }
- } catch (error) {
- console.error('Error fetching user profile:', error);
- }
- };
// Actions now handled by PageActions component
const handlePageUpdate = (updatedPage: Page) => {
- // Check if parent has changed
+ // If parent changed or critical metadata changed, strictly we should re-fetch to get enriched data
+ // But for responsiveness we can update local state.
+ // If parent changed, we don't have the new parent's title/slug unless we fetch it.
+ // So if parent ID changed, we should probably re-fetch the whole page data from server.
+
if (updatedPage.parent !== page?.parent) {
- if (!updatedPage.parent) {
- // Parent removed
- setPage({ ...updatedPage, parent_page: null });
- } else {
- // Parent changed, fetch new details
- setPage(updatedPage);
- fetchParentPage(updatedPage.parent);
+ // Re-fetch everything to get correct parent details
+ if (userId && updatedPage.slug) {
+ fetchUserPageData(userId, updatedPage.slug);
}
- // Also refresh children if needed, though usually this affects *other* pages listing this one as child
- // But if we became a child, we might want to check something.
- // Actually, if we are viewing this page, its children list shouldn't change just by changing its parent,
- // unless we selected one of our children as parent (which is forbidden by picker).
} else {
setPage(updatedPage);
}
@@ -305,6 +236,8 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
setPage({ ...page, title: titleValue.trim() });
setEditingTitle(false);
+ // Invalidate cache for this page
+ if (userId && page.slug) invalidateUserPageCache(userId, page.slug);
toast.success(translate('Title updated'));
} catch (error) {
console.error('Error updating title:', error);
@@ -360,6 +293,8 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
console.error('Error updating slug:', error);
toast.error(translate('Failed to update slug'));
} finally {
+ if (userId && page?.slug) invalidateUserPageCache(userId, page.slug); // Invalidate old slug
+ if (userId) invalidateUserPageCache(userId, slugValue.trim()); // Invalidate new slug to be safe
setSavingField(null);
}
};
@@ -390,6 +325,7 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
console.error('Error updating tags:', error);
toast.error(translate('Failed to update tags'));
} finally {
+ if (userId && page?.slug) invalidateUserPageCache(userId, page.slug);
setSavingField(null);
}
};
@@ -709,6 +645,7 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
pageName={page.title}
isEditMode={isEditMode && isOwner}
showControls={isEditMode && isOwner}
+ initialLayout={page.content}
/>
)}
diff --git a/packages/ui/src/pages/UserProfile.tsx b/packages/ui/src/pages/UserProfile.tsx
index 73b7ae13..f6afa6cb 100644
--- a/packages/ui/src/pages/UserProfile.tsx
+++ b/packages/ui/src/pages/UserProfile.tsx
@@ -11,6 +11,8 @@ import { ThemeToggle } from "@/components/ThemeToggle";
import UserPictures from "@/components/UserPictures";
import { T, translate } from "@/i18n";
import { normalizeMediaType } from "@/lib/mediaRegistry";
+import { useFeedData } from "@/hooks/useFeedData";
+import * as db from "@/lib/db";
interface UserProfile {
id: string;
@@ -42,6 +44,7 @@ const UserProfile = () => {
const navigate = useNavigate();
const { user: currentUser } = useAuth();
const [userProfile, setUserProfile] = useState