supabase fuck 2/5
This commit is contained in:
parent
ba9fba49cb
commit
4c734d4d3c
@ -77,7 +77,7 @@ const AppWrapper = () => {
|
||||
<Route path="/user/:userId" element={<UserProfile />} />
|
||||
<Route path="/user/:userId/collections" element={<UserCollections />} />
|
||||
<Route path="/user/:userId/pages/new" element={<NewPage />} />
|
||||
<Route path="/user/:userId/pages/:slug" element={<UserPage />} />
|
||||
<Route path="/user/:username/pages/:slug" element={<UserPage />} />
|
||||
<Route path="/collections/new" element={<NewCollection />} />
|
||||
<Route path="/collections/:userId/:slug" element={<Collections />} />
|
||||
<Route path="/tags/:tag" element={<TagPage />} />
|
||||
@ -109,7 +109,7 @@ const AppWrapper = () => {
|
||||
<Route path="/org/:orgSlug/user/:userId" element={<UserProfile />} />
|
||||
<Route path="/org/:orgSlug/user/:userId/collections" element={<UserCollections />} />
|
||||
<Route path="/org/:orgSlug/user/:userId/pages/new" element={<NewPage />} />
|
||||
<Route path="/org/:orgSlug/user/:userId/pages/:slug" element={<UserPage />} />
|
||||
<Route path="/org/:orgSlug/user/:username/pages/:slug" element={<UserPage />} />
|
||||
<Route path="/org/:orgSlug/collections/new" element={<NewCollection />} />
|
||||
<Route path="/org/:orgSlug/collections/:userId/:slug" element={<Collections />} />
|
||||
<Route path="/org/:orgSlug/tags/:tag" element={<TagPage />} />
|
||||
|
||||
@ -10,35 +10,16 @@ import { useFeedData } from "@/hooks/useFeedData";
|
||||
import { normalizeMediaType, isVideoType } from "@/lib/mediaRegistry";
|
||||
import { UserProfile } from '../pages/Post/types';
|
||||
import * as db from '../pages/Post/db';
|
||||
import type { MediaType } from "@/types";
|
||||
import type { MediaItem, MediaType } from "@/types";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
|
||||
// Duplicate types for now or we could reuse specific generic props
|
||||
// To minimalize refactoring PhotoGrid, I'll copy the logic but use the Feed variant
|
||||
|
||||
interface MediaItemType {
|
||||
id: string;
|
||||
picture_id?: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
image_url: string;
|
||||
thumbnail_url: string | null;
|
||||
type: MediaType;
|
||||
meta: any | null;
|
||||
likes_count: number;
|
||||
created_at: string;
|
||||
user_id: string;
|
||||
comments: { count: number }[];
|
||||
|
||||
author_profile?: UserProfile;
|
||||
job?: any;
|
||||
responsive?: any;
|
||||
}
|
||||
|
||||
import type { FeedSortOption } from '@/hooks/useFeedData';
|
||||
|
||||
interface GalleryLargeProps {
|
||||
customPictures?: MediaItemType[];
|
||||
customPictures?: MediaItem[];
|
||||
customLoading?: boolean;
|
||||
navigationSource?: 'home' | 'collection' | 'tag' | 'user';
|
||||
navigationSourceId?: string;
|
||||
@ -56,7 +37,7 @@ const GalleryLarge = ({
|
||||
const navigate = useNavigate();
|
||||
const { setNavigationData, navigationData } = usePostNavigation();
|
||||
const { orgSlug, isOrgContext } = useOrganization();
|
||||
const [mediaItems, setMediaItems] = useState<MediaItemType[]>([]);
|
||||
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
|
||||
const [userLikes, setUserLikes] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@ -73,7 +54,7 @@ const GalleryLarge = ({
|
||||
|
||||
// 2. State & Effects
|
||||
useEffect(() => {
|
||||
let finalMedia: MediaItemType[] = [];
|
||||
let finalMedia: MediaItem[] = [];
|
||||
|
||||
if (customPictures) {
|
||||
finalMedia = customPictures;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { translate } from '@/i18n';
|
||||
import { QuickAction } from '@/constants';
|
||||
import { PromptPreset } from '@/components/PresetManager';
|
||||
import { Workflow } from '@/components/WorkflowManager';
|
||||
import { getUserSettings, updateUserSettings } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* Settings Handlers
|
||||
@ -17,20 +17,13 @@ import { Workflow } from '@/components/WorkflowManager';
|
||||
|
||||
export const loadPromptTemplates = async (
|
||||
userId: string,
|
||||
setPromptTemplates: React.Dispatch<React.SetStateAction<Array<{name: string; template: string}>>>,
|
||||
setPromptTemplates: React.Dispatch<React.SetStateAction<Array<{ name: string; template: string }>>>,
|
||||
setLoadingTemplates: React.Dispatch<React.SetStateAction<boolean>>
|
||||
) => {
|
||||
setLoadingTemplates(true);
|
||||
console.log('loading prompt templates');
|
||||
try {
|
||||
const { data: profile, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error;
|
||||
|
||||
const settings = profile?.settings as any;
|
||||
const settings = await getUserSettings(userId);
|
||||
const templates = settings?.promptTemplates || [];
|
||||
setPromptTemplates(templates);
|
||||
} catch (error) {
|
||||
@ -42,29 +35,16 @@ export const loadPromptTemplates = async (
|
||||
|
||||
export const savePromptTemplates = async (
|
||||
userId: string,
|
||||
templates: Array<{name: string; template: string}>
|
||||
templates: Array<{ name: string; template: string }>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
||||
|
||||
const currentSettings = (profile?.settings as any) || {};
|
||||
const currentSettings = await getUserSettings(userId);
|
||||
const newSettings = {
|
||||
...currentSettings,
|
||||
promptTemplates: templates
|
||||
};
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({ settings: newSettings as any })
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, newSettings);
|
||||
|
||||
toast.success(translate('Templates saved successfully!'));
|
||||
} catch (error) {
|
||||
@ -81,15 +61,7 @@ export const loadPromptPresets = async (
|
||||
) => {
|
||||
setLoadingPresets(true);
|
||||
try {
|
||||
const { data: profile, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error;
|
||||
|
||||
const settings = profile?.settings as any;
|
||||
const settings = await getUserSettings(userId);
|
||||
const presets = settings?.promptPresets || [];
|
||||
setPromptPresets(presets);
|
||||
} catch (error) {
|
||||
@ -106,15 +78,7 @@ export const loadWorkflows = async (
|
||||
) => {
|
||||
setLoadingWorkflows(true);
|
||||
try {
|
||||
const { data: profile, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error;
|
||||
|
||||
const settings = profile?.settings as any;
|
||||
const settings = await getUserSettings(userId);
|
||||
const workflows = settings?.workflows || [];
|
||||
setWorkflows(workflows);
|
||||
} catch (error) {
|
||||
@ -132,15 +96,7 @@ export const loadQuickActions = async (
|
||||
) => {
|
||||
setLoadingActions(true);
|
||||
try {
|
||||
const { data: profile, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error;
|
||||
|
||||
const settings = profile?.settings as any;
|
||||
const settings = await getUserSettings(userId);
|
||||
const actions = settings?.quickActions || DEFAULT_QUICK_ACTIONS;
|
||||
setQuickActions(actions);
|
||||
} catch (error) {
|
||||
@ -156,27 +112,12 @@ export const saveQuickActions = async (
|
||||
actions: QuickAction[]
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
const currentSettings = await getUserSettings(userId);
|
||||
|
||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
||||
|
||||
const currentSettings = (profile?.settings as any) || {};
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...currentSettings,
|
||||
quickActions: actions
|
||||
} as any
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...currentSettings,
|
||||
quickActions: actions
|
||||
});
|
||||
|
||||
toast.success('Quick Actions saved');
|
||||
} catch (error) {
|
||||
@ -191,15 +132,7 @@ export const loadPromptHistory = async (
|
||||
setPromptHistory: React.Dispatch<React.SetStateAction<string[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error;
|
||||
|
||||
const settings = profile?.settings as any;
|
||||
const settings = await getUserSettings(userId);
|
||||
const history = settings?.promptHistory || [];
|
||||
setPromptHistory(history);
|
||||
} catch (error) {
|
||||
@ -214,33 +147,18 @@ export const addToPromptHistory = async (
|
||||
setHistoryIndex: React.Dispatch<React.SetStateAction<number>>
|
||||
) => {
|
||||
if (!promptText.trim()) return;
|
||||
|
||||
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
||||
|
||||
const currentSettings = (profile?.settings as any) || {};
|
||||
const currentSettings = await getUserSettings(userId);
|
||||
const currentHistory = currentSettings.promptHistory || [];
|
||||
|
||||
|
||||
// Add to history, avoid duplicates, limit to 50
|
||||
const newHistory = [promptText, ...currentHistory.filter((h: string) => h !== promptText)].slice(0, 50);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...currentSettings,
|
||||
promptHistory: newHistory
|
||||
} as any
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...currentSettings,
|
||||
promptHistory: newHistory
|
||||
});
|
||||
|
||||
setPromptHistory(newHistory);
|
||||
setHistoryIndex(-1); // Reset to latest
|
||||
@ -257,9 +175,9 @@ export const navigatePromptHistory = (
|
||||
setPrompt: React.Dispatch<React.SetStateAction<string>>
|
||||
) => {
|
||||
if (promptHistory.length === 0) return;
|
||||
|
||||
|
||||
let newIndex = historyIndex;
|
||||
|
||||
|
||||
if (direction === 'up') {
|
||||
// Go back in history
|
||||
newIndex = Math.min(historyIndex + 1, promptHistory.length - 1);
|
||||
@ -267,9 +185,9 @@ export const navigatePromptHistory = (
|
||||
// Go forward in history
|
||||
newIndex = Math.max(historyIndex - 1, -1);
|
||||
}
|
||||
|
||||
|
||||
setHistoryIndex(newIndex);
|
||||
|
||||
|
||||
if (newIndex === -1) {
|
||||
setPrompt('');
|
||||
} else {
|
||||
@ -287,15 +205,7 @@ export const savePromptPreset = async (
|
||||
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
||||
|
||||
const settings = (profile?.settings as any) || {};
|
||||
const settings = await getUserSettings(userId);
|
||||
const existingPresets = settings.promptPresets || [];
|
||||
|
||||
const newPreset: PromptPreset = {
|
||||
@ -307,17 +217,10 @@ export const savePromptPreset = async (
|
||||
|
||||
const updatedPresets = [...existingPresets, newPreset];
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...settings,
|
||||
promptPresets: updatedPresets,
|
||||
},
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...settings,
|
||||
promptPresets: updatedPresets,
|
||||
});
|
||||
|
||||
setPromptPresets(updatedPresets);
|
||||
toast.success(translate('Preset saved successfully!'));
|
||||
@ -335,15 +238,7 @@ export const updatePromptPreset = async (
|
||||
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
const settings = (profile?.settings as any) || {};
|
||||
const settings = await getUserSettings(userId);
|
||||
const existingPresets = settings.promptPresets || [];
|
||||
|
||||
const updatedPresets = existingPresets.map((p: PromptPreset) =>
|
||||
@ -352,17 +247,10 @@ export const updatePromptPreset = async (
|
||||
: p
|
||||
);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...settings,
|
||||
promptPresets: updatedPresets,
|
||||
},
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...settings,
|
||||
promptPresets: updatedPresets,
|
||||
});
|
||||
|
||||
setPromptPresets(updatedPresets);
|
||||
toast.success(translate('Preset updated successfully!'));
|
||||
@ -379,30 +267,15 @@ export const deletePromptPreset = async (
|
||||
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
const settings = (profile?.settings as any) || {};
|
||||
const settings = await getUserSettings(userId);
|
||||
const existingPresets = settings.promptPresets || [];
|
||||
|
||||
const updatedPresets = existingPresets.filter((p: PromptPreset) => p.id !== id);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...settings,
|
||||
promptPresets: updatedPresets,
|
||||
},
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...settings,
|
||||
promptPresets: updatedPresets,
|
||||
});
|
||||
|
||||
setPromptPresets(updatedPresets);
|
||||
toast.success(translate('Preset deleted successfully!'));
|
||||
@ -423,15 +296,7 @@ export const saveWorkflow = async (
|
||||
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
||||
|
||||
const settings = (profile?.settings as any) || {};
|
||||
const settings = await getUserSettings(userId);
|
||||
const existingWorkflows = settings.workflows || [];
|
||||
|
||||
const newWorkflow: Workflow = {
|
||||
@ -443,17 +308,10 @@ export const saveWorkflow = async (
|
||||
|
||||
const updatedWorkflows = [...existingWorkflows, newWorkflow];
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...settings,
|
||||
workflows: updatedWorkflows,
|
||||
},
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...settings,
|
||||
workflows: updatedWorkflows,
|
||||
});
|
||||
|
||||
setWorkflows(updatedWorkflows);
|
||||
toast.success(translate('Workflow saved successfully!'));
|
||||
@ -471,15 +329,7 @@ export const updateWorkflow = async (
|
||||
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
const settings = (profile?.settings as any) || {};
|
||||
const settings = await getUserSettings(userId);
|
||||
const existingWorkflows = settings.workflows || [];
|
||||
|
||||
const updatedWorkflows = existingWorkflows.map((w: Workflow) =>
|
||||
@ -488,17 +338,10 @@ export const updateWorkflow = async (
|
||||
: w
|
||||
);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...settings,
|
||||
workflows: updatedWorkflows,
|
||||
},
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...settings,
|
||||
workflows: updatedWorkflows,
|
||||
});
|
||||
|
||||
setWorkflows(updatedWorkflows);
|
||||
toast.success(translate('Workflow updated successfully!'));
|
||||
@ -515,30 +358,15 @@ export const deleteWorkflow = async (
|
||||
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
||||
) => {
|
||||
try {
|
||||
const { data: profile, error: fetchError } = await supabase
|
||||
.from('profiles')
|
||||
.select('settings')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
const settings = (profile?.settings as any) || {};
|
||||
const settings = await getUserSettings(userId);
|
||||
const existingWorkflows = settings.workflows || [];
|
||||
|
||||
const updatedWorkflows = existingWorkflows.filter((w: Workflow) => w.id !== id);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
settings: {
|
||||
...settings,
|
||||
workflows: updatedWorkflows,
|
||||
},
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
await updateUserSettings(userId, {
|
||||
...settings,
|
||||
workflows: updatedWorkflows,
|
||||
});
|
||||
|
||||
setWorkflows(updatedWorkflows);
|
||||
toast.success(translate('Workflow deleted successfully!'));
|
||||
|
||||
@ -34,6 +34,7 @@ interface MediaCardProps {
|
||||
job?: any;
|
||||
variant?: 'grid' | 'feed';
|
||||
apiUrl?: string;
|
||||
versionCount?: number;
|
||||
}
|
||||
|
||||
const MediaCard: React.FC<MediaCardProps> = ({
|
||||
@ -60,7 +61,8 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
||||
responsive,
|
||||
job,
|
||||
variant = 'grid',
|
||||
apiUrl
|
||||
apiUrl,
|
||||
versionCount
|
||||
}) => {
|
||||
const normalizedType = normalizeMediaType(type);
|
||||
// Render based on type
|
||||
@ -151,6 +153,7 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
||||
responsive={responsive}
|
||||
variant={variant}
|
||||
apiUrl={apiUrl}
|
||||
versionCount={versionCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -14,6 +14,7 @@ interface PageCardProps extends Omit<MediaRendererProps, 'created_at'> {
|
||||
authorAvatarUrl?: string | null;
|
||||
created_at?: string;
|
||||
apiUrl?: string;
|
||||
versionCount?: number;
|
||||
}
|
||||
|
||||
const PageCard: React.FC<PageCardProps> = ({
|
||||
@ -34,11 +35,13 @@ const PageCard: React.FC<PageCardProps> = ({
|
||||
variant = 'grid',
|
||||
responsive,
|
||||
showContent = true,
|
||||
apiUrl
|
||||
apiUrl,
|
||||
versionCount
|
||||
}) => {
|
||||
// Determine image source
|
||||
// If url is missing or empty, fallback to picsum
|
||||
// For PAGE_EXTERNAL, currently 'url' is the link and 'thumbnailUrl' is the image.
|
||||
|
||||
const displayImage = thumbnailUrl || url || "https://picsum.photos/640";
|
||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||
|
||||
@ -47,7 +50,6 @@ const PageCard: React.FC<PageCardProps> = ({
|
||||
const ytId = getYouTubeVideoId(url);
|
||||
const isExternalVideo = !!(tikTokId || ytId);
|
||||
|
||||
|
||||
// Use thumbnail if available and preferred (logic from MediaCard usually handles this before passing url,
|
||||
// but here we ensure we have *something*).
|
||||
|
||||
|
||||
@ -41,11 +41,12 @@ interface Page {
|
||||
|
||||
interface PageManagerProps {
|
||||
userId: string;
|
||||
username?: string;
|
||||
isOwnProfile: boolean;
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
const PageManager = ({ userId, username, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [pages, setPages] = useState<Page[]>([]);
|
||||
@ -60,7 +61,7 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
const fetchPages = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
let query = supabase
|
||||
.from('pages')
|
||||
.select('*')
|
||||
@ -118,7 +119,7 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setPages(pages.map(p =>
|
||||
setPages(pages.map(p =>
|
||||
p.id === page.id ? { ...p, visible: !p.visible } : p
|
||||
));
|
||||
toast.success(translate(page.visible ? 'Page hidden' : 'Page made visible'));
|
||||
@ -140,7 +141,7 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setPages(pages.map(p =>
|
||||
setPages(pages.map(p =>
|
||||
p.id === page.id ? { ...p, is_public: !p.is_public } : p
|
||||
));
|
||||
toast.success(translate(page.is_public ? 'Page made private' : 'Page made public'));
|
||||
@ -152,9 +153,9 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
|
||||
const getPageUrl = (slug: string) => {
|
||||
if (orgSlug) {
|
||||
return `/org/${orgSlug}/user/${userId}/pages/${slug}`;
|
||||
return `/org/${orgSlug}/user/${username || userId}/pages/${slug}`;
|
||||
}
|
||||
return `/user/${userId}/pages/${slug}`;
|
||||
return `/user/${username || userId}/pages/${slug}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@ -174,13 +175,13 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold"><T>Pages</T></h3>
|
||||
{isOwnProfile && (
|
||||
<Button
|
||||
size="sm"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const newPageUrl = orgSlug
|
||||
? `/org/${orgSlug}/user/${userId}/pages/new`
|
||||
: `/user/${userId}/pages/new`;
|
||||
const newPageUrl = orgSlug
|
||||
? `/org/${orgSlug}/user/${username || userId}/pages/new`
|
||||
: `/user/${username || userId}/pages/new`;
|
||||
navigate(newPageUrl);
|
||||
}}
|
||||
>
|
||||
@ -207,7 +208,7 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-base">
|
||||
<Link
|
||||
<Link
|
||||
to={getPageUrl(page.slug)}
|
||||
className="hover:text-primary transition-colors"
|
||||
>
|
||||
|
||||
@ -38,6 +38,7 @@ interface PhotoCardProps {
|
||||
responsive?: any;
|
||||
variant?: 'grid' | 'feed';
|
||||
apiUrl?: string;
|
||||
versionCount?: number;
|
||||
}
|
||||
|
||||
const PhotoCard = ({
|
||||
@ -60,7 +61,8 @@ const PhotoCard = ({
|
||||
showContent = true,
|
||||
responsive,
|
||||
variant = 'grid',
|
||||
apiUrl
|
||||
apiUrl,
|
||||
versionCount
|
||||
}: PhotoCardProps) => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
@ -72,48 +74,28 @@ const PhotoCard = ({
|
||||
const [showLightbox, setShowLightbox] = useState(false);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [isPublishing, setIsPublishing] = useState(false);
|
||||
|
||||
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | null>(null);
|
||||
const [versionCount, setVersionCount] = useState<number>(0);
|
||||
|
||||
const isOwner = user?.id === authorId;
|
||||
|
||||
// Fetch version count for owners only
|
||||
const [localVersionCount, setLocalVersionCount] = useState<number>(versionCount || 0);
|
||||
|
||||
// Sync prop to state if needed, or just use prop.
|
||||
// If we want to allow local updates (e.g. after adding a version), we can keep state.
|
||||
useEffect(() => {
|
||||
const fetchVersionCount = async () => {
|
||||
if (!isOwner || !user) return;
|
||||
if (versionCount !== undefined) {
|
||||
setLocalVersionCount(versionCount);
|
||||
}
|
||||
}, [versionCount]);
|
||||
|
||||
try {
|
||||
// Count pictures that have this picture as parent OR pictures that share the same parent
|
||||
const { data: currentPicture } = await supabase
|
||||
.from('pictures')
|
||||
.select('parent_id')
|
||||
.eq('id', pictureId)
|
||||
.single();
|
||||
|
||||
if (!currentPicture) return;
|
||||
|
||||
let query = supabase
|
||||
.from('pictures')
|
||||
.select('id', { count: 'exact', head: true });
|
||||
|
||||
if (currentPicture.parent_id) {
|
||||
// This is a version - count all versions with same parent_id + the parent itself
|
||||
query = query.or(`parent_id.eq.${currentPicture.parent_id},id.eq.${currentPicture.parent_id}`);
|
||||
} else {
|
||||
// This is the original - count this picture + all its versions
|
||||
query = query.or(`parent_id.eq.${pictureId},id.eq.${pictureId}`);
|
||||
}
|
||||
|
||||
const { count } = await query;
|
||||
// console.log('Version count:', count);
|
||||
setVersionCount(count || 1);
|
||||
} catch (error) {
|
||||
console.error('Error fetching version count:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchVersionCount();
|
||||
// Legacy fetch removed in favor of passed prop
|
||||
/*
|
||||
useEffect(() => {
|
||||
// ... legacy fetch logic ...
|
||||
}, [pictureId, isOwner, user]);
|
||||
*/
|
||||
|
||||
const handleLike = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@ -488,10 +470,10 @@ const PhotoCard = ({
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{versionCount > 1 && (
|
||||
{localVersionCount > 1 && (
|
||||
<div className="flex items-center ml-2 px-2 py-1 bg-white/20 rounded text-white text-xs">
|
||||
<Layers className="h-3 w-3 mr-1" />
|
||||
{versionCount}
|
||||
{localVersionCount}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -31,9 +31,10 @@ export interface MediaItemType {
|
||||
user_id: string;
|
||||
comments: { count: number }[];
|
||||
|
||||
author_profile?: UserProfile;
|
||||
author?: UserProfile;
|
||||
job?: any;
|
||||
responsive?: any; // Add responsive data
|
||||
versionCount?: number;
|
||||
}
|
||||
|
||||
import type { FeedSortOption } from '@/hooks/useFeedData';
|
||||
@ -98,7 +99,6 @@ const MediaGrid = ({
|
||||
enabled: !customPictures,
|
||||
supabaseClient
|
||||
});
|
||||
|
||||
// Infinite Scroll Observer
|
||||
const observerTarget = useRef(null);
|
||||
|
||||
@ -182,6 +182,8 @@ const MediaGrid = ({
|
||||
hasRestoredScroll.current = false;
|
||||
}, [cacheKey]);
|
||||
|
||||
|
||||
|
||||
// Track scroll position
|
||||
const lastScrollY = useRef(window.scrollY);
|
||||
|
||||
@ -252,8 +254,9 @@ const MediaGrid = ({
|
||||
// Handle Page navigation
|
||||
if (type === 'page-intern') {
|
||||
const item = mediaItems.find(i => i.id === mediaId);
|
||||
|
||||
if (item && item.meta?.slug) {
|
||||
navigate(`/user/${item.user_id}/pages/${item.meta.slug}`);
|
||||
navigate(`/user/${item.author?.username || item.user_id}/pages/${item.meta.slug}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -382,7 +385,6 @@ const MediaGrid = ({
|
||||
}
|
||||
|
||||
const hasItems = mediaItems.length > 0;
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
{hasItems && isOwner && onFilesDrop && navigationSource === 'collection' && (
|
||||
|
||||
@ -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 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/user/${username}`)}
|
||||
className="h-8 w-8 p-0"
|
||||
title={translate("My Profile")}
|
||||
>
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={`/user/${username}`} className="flex items-center">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<T>Profile</T>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
const handleLanguageChange = (langCode: string) => {
|
||||
setLanguage(langCode as any);
|
||||
};
|
||||
|
||||
@ -50,7 +50,13 @@ const UserAvatarBlock: React.FC<UserAvatarBlockProps> = ({
|
||||
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
|
||||
|
||||
@ -24,17 +24,20 @@ export const FeedCard: React.FC<FeedCardProps> = ({
|
||||
onNavigate
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [isLiked, setIsLiked] = useState<boolean>(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<boolean>(initialLiked);
|
||||
const [likeCount, setLikeCount] = useState(post.likes_count || 0);
|
||||
const [lastTap, setLastTap] = useState<number>(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<FeedCardProps> = ({
|
||||
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<FeedCardProps> = ({
|
||||
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}
|
||||
/>
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ interface GenericCanvasProps {
|
||||
className?: string;
|
||||
selectedWidgetId?: string | null;
|
||||
onSelectWidget?: (widgetId: string) => void;
|
||||
initialLayout?: any;
|
||||
}
|
||||
|
||||
const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
||||
@ -24,11 +25,13 @@ const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
||||
showControls = true,
|
||||
className = '',
|
||||
selectedWidgetId,
|
||||
onSelectWidget
|
||||
onSelectWidget,
|
||||
initialLayout
|
||||
}) => {
|
||||
const {
|
||||
loadedPages,
|
||||
loadPageLayout,
|
||||
hydratePageLayout,
|
||||
addWidgetToPage,
|
||||
removeWidgetFromPage,
|
||||
moveWidgetInPage,
|
||||
@ -45,11 +48,17 @@ const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
||||
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<string | null>(null);
|
||||
const [showWidgetPalette, setShowWidgetPalette] = useState(false);
|
||||
|
||||
@ -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<MarkdownTextWidgetProps> = ({
|
||||
// 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<MarkdownTextWidgetProps> = ({
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [user]);
|
||||
}, [user, isEditMode]);
|
||||
|
||||
// Save AI Text Generator settings to profile (debounced)
|
||||
useEffect(() => {
|
||||
@ -133,13 +127,7 @@ const MarkdownTextWidget: React.FC<MarkdownTextWidgetProps> = ({
|
||||
|
||||
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<MarkdownTextWidgetProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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<MarkdownTextWidgetProps> = ({
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
@ -21,6 +21,7 @@ interface LayoutContextType {
|
||||
renameWidget: (pageId: string, widgetInstanceId: string, newId: string) => Promise<boolean>;
|
||||
exportPageLayout: (pageId: string) => Promise<string>;
|
||||
importPageLayout: (pageId: string, jsonData: string) => Promise<PageLayout>;
|
||||
hydratePageLayout: (pageId: string, layout: PageLayout) => void;
|
||||
|
||||
// Manual save
|
||||
saveToApi: () => Promise<boolean>;
|
||||
@ -383,6 +384,14 @@ export const LayoutProvider: React.FC<LayoutProviderProps> = ({ 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<boolean> => {
|
||||
try {
|
||||
// Save all loaded pages to database
|
||||
@ -417,6 +426,7 @@ export const LayoutProvider: React.FC<LayoutProviderProps> = ({ children }) => {
|
||||
updateWidgetProps,
|
||||
exportPageLayout,
|
||||
importPageLayout,
|
||||
hydratePageLayout,
|
||||
saveToApi,
|
||||
isLoading,
|
||||
loadedPages,
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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<string, string> = {};
|
||||
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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<string | null> => {
|
||||
try {
|
||||
@ -44,19 +46,7 @@ export const getGoogleApiKey = async (): Promise<string | null> => {
|
||||
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<string, string> } | 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.');
|
||||
|
||||
@ -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<string, Promise<any>>();
|
||||
|
||||
const fetchWithDeduplication = async <T>(key: string, fetcher: () => Promise<T>): Promise<T> => {
|
||||
type CacheStorageType = 'memory' | 'local';
|
||||
|
||||
interface StoredCacheItem<T> {
|
||||
value: T;
|
||||
timestamp: number;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
const fetchWithDeduplication = async <T>(
|
||||
key: string,
|
||||
fetcher: () => Promise<T>,
|
||||
timeout: number = 25000,
|
||||
storage: CacheStorageType = 'local'
|
||||
): Promise<T> => {
|
||||
// 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<T> = 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<T> = {
|
||||
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<T>;
|
||||
};
|
||||
|
||||
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<PostMediaItem>[], 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<FeedPost[]> => {
|
||||
// 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;
|
||||
|
||||
@ -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<string | null> => {
|
||||
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<string, string> } | 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.');
|
||||
|
||||
@ -68,7 +68,7 @@ export function extractHeadingsFromLayout(layout: PageLayout): MarkdownHeading[]
|
||||
container.children.forEach(processContainer);
|
||||
};
|
||||
|
||||
layout.containers.forEach(processContainer);
|
||||
layout.containers?.forEach(processContainer);
|
||||
|
||||
return allHeadings;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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<string[]>([]);
|
||||
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<Array<{ name: string; template: string }>>([]);
|
||||
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<string | null> => {
|
||||
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}
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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<string, string> = {};
|
||||
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();
|
||||
};
|
||||
|
||||
@ -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<string | null>(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<Page | null>(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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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<UserProfile | null>(null);
|
||||
console.log(userProfile, "userProfile");
|
||||
|
||||
// Post states
|
||||
const [publicPosts, setPublicPosts] = useState<any[]>([]);
|
||||
@ -49,25 +52,69 @@ const UserProfile = () => {
|
||||
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'posts' | 'hidden' | 'pictures'>('posts');
|
||||
|
||||
const isOwnProfile = currentUser?.id === userId;
|
||||
|
||||
// Post states handled by useFeedData
|
||||
const { posts: feedPosts, loading: feedLoading } = useFeedData({
|
||||
source: 'user',
|
||||
sourceId: userId,
|
||||
enabled: !!userId,
|
||||
sortBy: 'latest'
|
||||
});
|
||||
|
||||
const [stats, setStats] = useState({ public: 0, hidden: 0, total: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
fetchUserProfile();
|
||||
fetchUserPosts();
|
||||
fetchUserCollections();
|
||||
fetchUserOrganizations();
|
||||
fetchUserStats();
|
||||
}
|
||||
}, [userId, currentUser]);
|
||||
}, [userId]); // Removed userProfile and currentUser from deps to prevent loop
|
||||
|
||||
useEffect(() => {
|
||||
if (feedPosts) {
|
||||
// Separate into Public/Listed vs Private (Hidden)
|
||||
// Note: feedPosts are already FeedPost objects, map them or use as is?
|
||||
// PhotoGrid expects MediaItemType via mapFeedPostsToMediaItems, but it handles FeedPost[] via customPictures?
|
||||
// Actually customPictures expects MediaItemType[].
|
||||
// We need to map them.
|
||||
const mediaItems = db.mapFeedPostsToMediaItems(feedPosts, 'latest');
|
||||
|
||||
const publicAndListed = mediaItems.filter((p: any) => !p.meta?.visibility || p.meta?.visibility !== 'private');
|
||||
// Need to check where visibility is stored. FeedPost has settings.
|
||||
// mapFeedPostsToMediaItems might lose settings?
|
||||
// Let's check mapFeedPostsToMediaItems. It maps 'meta' from 'cover.meta'.
|
||||
// Visibility is usually on the post 'settings'. mapFeedPostsToMediaItems doesn't seem to pass 'settings' or 'visibility' explicitly?
|
||||
// We might need to filter on feedPosts first then map.
|
||||
|
||||
const publicFeed = feedPosts.filter(p => !p.settings?.visibility || p.settings.visibility !== 'private');
|
||||
const hiddenFeed = feedPosts.filter(p => p.settings?.visibility === 'private');
|
||||
|
||||
setPublicPosts(db.mapFeedPostsToMediaItems(publicFeed, 'latest'));
|
||||
setHiddenPosts(db.mapFeedPostsToMediaItems(hiddenFeed, 'latest'));
|
||||
}
|
||||
}, [feedPosts]);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
console.log('fetchUserProfile', userId);
|
||||
try {
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.select(`
|
||||
*,
|
||||
user_roles (role),
|
||||
user_organizations (
|
||||
role,
|
||||
organizations (
|
||||
id,
|
||||
name,
|
||||
slug
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
@ -75,25 +122,44 @@ const UserProfile = () => {
|
||||
throw profileError;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
setUserProfile({
|
||||
id: userId!,
|
||||
username: null,
|
||||
display_name: `User ${userId!.slice(0, 8)}`,
|
||||
bio: null,
|
||||
avatar_url: null,
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
const newProfile = profile ? {
|
||||
id: profile.user_id,
|
||||
username: profile.username,
|
||||
display_name: profile.display_name || `User ${userId!.slice(0, 8)}`,
|
||||
bio: profile.bio,
|
||||
avatar_url: profile.avatar_url,
|
||||
created_at: profile.created_at,
|
||||
} : {
|
||||
id: userId!,
|
||||
username: null,
|
||||
display_name: `User ${userId!.slice(0, 8)}`,
|
||||
bio: null,
|
||||
avatar_url: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setUserProfile(prev => JSON.stringify(prev) !== JSON.stringify(newProfile) ? newProfile : prev);
|
||||
|
||||
// Process Orgs and Roles from the profile fetch
|
||||
if (profile) {
|
||||
// Process Organizations
|
||||
const orgsData = (profile as any).user_organizations || [];
|
||||
const orgs = orgsData.map((item: any) => ({
|
||||
id: item.organizations?.id,
|
||||
name: item.organizations?.name,
|
||||
slug: item.organizations?.slug,
|
||||
role: item.role
|
||||
})).filter((o: any) => o.id); // Filter out invalid
|
||||
|
||||
setOrganizations(orgs);
|
||||
|
||||
// Process Roles (if we want to store them, user didn't ask for UI but asked for resolving)
|
||||
// const roles = (profile as any).user_roles?.map((r: any) => r.role) || [];
|
||||
// setRoles(roles); // If we had a roles state
|
||||
} else {
|
||||
setUserProfile({
|
||||
id: profile.user_id,
|
||||
username: profile.username,
|
||||
display_name: profile.display_name || `User ${userId!.slice(0, 8)}`,
|
||||
bio: profile.bio,
|
||||
avatar_url: profile.avatar_url,
|
||||
created_at: profile.created_at,
|
||||
});
|
||||
setOrganizations([]);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user profile:', error);
|
||||
toast.error(translate('Failed to load user profile'));
|
||||
@ -101,6 +167,30 @@ const UserProfile = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserStats = async () => {
|
||||
try {
|
||||
const { count: publicCount } = await supabase
|
||||
.from('posts')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('user_id', userId)
|
||||
.or('settings.is.null,settings->>visibility.eq.public');
|
||||
|
||||
const { count: hiddenCount } = await supabase
|
||||
.from('posts')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('user_id', userId)
|
||||
.eq('settings->>visibility', 'private');
|
||||
|
||||
setStats({
|
||||
public: publicCount || 0,
|
||||
hidden: hiddenCount || 0,
|
||||
total: (publicCount || 0) + (hiddenCount || 0)
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error fetching stats", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserCollections = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
@ -116,106 +206,9 @@ const UserProfile = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserOrganizations = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('user_organizations')
|
||||
.select(`
|
||||
role,
|
||||
organizations (
|
||||
id,
|
||||
name,
|
||||
slug
|
||||
)
|
||||
`)
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const orgs = data?.map(item => ({
|
||||
id: (item.organizations as any).id,
|
||||
name: (item.organizations as any).name,
|
||||
slug: (item.organizations as any).slug,
|
||||
role: item.role
|
||||
})) || [];
|
||||
|
||||
setOrganizations(orgs);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user organizations:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
let query = supabase
|
||||
.from('posts')
|
||||
.select(`
|
||||
*,
|
||||
pictures (
|
||||
id,
|
||||
image_url,
|
||||
thumbnail_url,
|
||||
type,
|
||||
meta,
|
||||
description,
|
||||
likes_count,
|
||||
visible,
|
||||
is_selected
|
||||
)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
const { data: posts, error } = await query;
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Transform posts for Grid
|
||||
const transformedPosts = (posts || []).map((post: any) => {
|
||||
// Find main picture (first one)
|
||||
const mainPicture = post.pictures?.[0];
|
||||
|
||||
return {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
// Use thumbnail_url if available (videos), else image_url
|
||||
image_url: mainPicture?.thumbnail_url || mainPicture?.image_url || '',
|
||||
thumbnail_url: mainPicture?.thumbnail_url || mainPicture?.image_url || null,
|
||||
type: normalizeMediaType(mainPicture?.type),
|
||||
meta: mainPicture?.meta,
|
||||
likes_count: 0, // aggregate likes count if needed, or just 0 for now
|
||||
created_at: post.created_at,
|
||||
user_id: post.user_id,
|
||||
comments: [{ count: 0 }], // placeholder
|
||||
settings: post.settings
|
||||
};
|
||||
}).filter(p => p.image_url); // Filter out posts without media for now
|
||||
|
||||
// Separate into Public/Listed vs Private (Hidden)
|
||||
const publicAndListed = transformedPosts.filter(p => !p.settings?.visibility || p.settings.visibility !== 'private');
|
||||
const privateHidden = transformedPosts.filter(p => p.settings?.visibility === 'private');
|
||||
|
||||
setPublicPosts(publicAndListed);
|
||||
setHiddenPosts(privateHidden);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user posts:', error);
|
||||
toast.error(translate('Failed to load user posts'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-muted-foreground"><T>Loading profile...</T></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!userProfile) {
|
||||
return (
|
||||
@ -299,7 +292,7 @@ const UserProfile = () => {
|
||||
{/* Stats */}
|
||||
<div className="flex justify-center md:justify-start gap-8 mb-6">
|
||||
<div className="text-center">
|
||||
<div className="font-semibold text-lg">{publicPosts.length + (isOwnProfile ? hiddenPosts.length : 0)}</div>
|
||||
<div className="font-semibold text-lg">{stats.total}</div>
|
||||
<div className="text-sm text-muted-foreground"><T>posts</T></div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
@ -333,7 +326,7 @@ const UserProfile = () => {
|
||||
<div className="flex gap-4 overflow-x-auto pb-2">
|
||||
{/* View All Collections Link */}
|
||||
<Link
|
||||
to={`/user/${userId}/collections`}
|
||||
to={`/user/${userProfile.username || userId}/collections`}
|
||||
className="flex flex-col items-center gap-2 min-w-0 group"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full border-2 border-primary bg-gradient-primary flex items-center justify-center group-hover:scale-105 transition-transform">
|
||||
@ -361,7 +354,7 @@ const UserProfile = () => {
|
||||
{collections.slice(0, 3).map((collection) => (
|
||||
<Link
|
||||
key={collection.id}
|
||||
to={`/collections/${userId}/${collection.slug}`}
|
||||
to={`/collections/${userProfile.username || userId}/${collection.slug}`}
|
||||
className="flex flex-col items-center gap-2 min-w-0 group"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full border-2 border-primary/30 bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center group-hover:scale-105 transition-transform">
|
||||
@ -417,7 +410,7 @@ const UserProfile = () => {
|
||||
|
||||
{/* Pages Section */}
|
||||
<div className="border-t p-2">
|
||||
<PageManager userId={userId!} isOwnProfile={isOwnProfile} orgSlug={orgSlug} />
|
||||
<PageManager userId={userId!} username={userProfile.username || undefined} isOwnProfile={isOwnProfile} orgSlug={orgSlug} />
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
@ -472,12 +465,12 @@ const UserProfile = () => {
|
||||
<div>
|
||||
<PhotoGrid
|
||||
customPictures={activeTab === 'posts' ? publicPosts : hiddenPosts}
|
||||
customLoading={loading}
|
||||
customLoading={feedLoading}
|
||||
isOwner={isOwnProfile}
|
||||
navigationSource="user"
|
||||
navigationSourceId={userId}
|
||||
/>
|
||||
{isOwnProfile && activeTab === 'posts' && publicPosts.length === 0 && !loading && (
|
||||
{isOwnProfile && activeTab === 'posts' && publicPosts.length === 0 && !feedLoading && (
|
||||
<div className="text-center mt-8">
|
||||
<Button
|
||||
className="bg-primary hover:bg-primary/90 text-white"
|
||||
|
||||
@ -12,7 +12,7 @@ export interface NavigationPost {
|
||||
export interface NavigationData {
|
||||
posts: NavigationPost[];
|
||||
currentIndex: number;
|
||||
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid';
|
||||
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid' | 'widget';
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
/// <reference lib="webworker" />
|
||||
import { clientsClaim } from 'workbox-core'
|
||||
import { NetworkFirst } from 'workbox-strategies';
|
||||
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
|
||||
import { registerRoute, NavigationRoute } from 'workbox-routing'
|
||||
import { set } from 'idb-keyval'
|
||||
|
||||
const SW_VERSION = '1.0.5-debug';
|
||||
|
||||
const SW_VERSION = '1.0.4-debug';
|
||||
console.log(`[SW] Initializing Version: ${SW_VERSION}`);
|
||||
|
||||
self.addEventListener('fetch', (event) => { });
|
||||
|
||||
import { cleanupOutdatedCaches, cleanupOutdatedCaches as cleanupOutdatedCaches2, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
|
||||
import { registerRoute, NavigationRoute } from 'workbox-routing'
|
||||
import { set } from 'idb-keyval'
|
||||
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
@ -29,7 +30,7 @@ clientsClaim()
|
||||
|
||||
// allow only fallback in dev: we don't want to cache everything
|
||||
let allowlist: undefined | RegExp[]
|
||||
if (import.meta.env.DEV)
|
||||
if (location.hostname === 'localhost' || location.hostname.includes('127.0.0.1'))
|
||||
allowlist = [/^\/$/]
|
||||
|
||||
// Handle Share Target POST requests
|
||||
@ -63,7 +64,6 @@ registerRoute(
|
||||
'POST'
|
||||
);
|
||||
|
||||
import { NetworkFirst } from 'workbox-strategies';
|
||||
|
||||
// Navigation handler: Prefer network to get server injection, fallback to index.html
|
||||
const navigationHandler = async (params: any) => {
|
||||
|
||||
@ -23,6 +23,7 @@ export interface MediaItem {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
likes_count: number | null;
|
||||
is_liked?: boolean;
|
||||
is_selected: boolean;
|
||||
visible: boolean;
|
||||
tags: string[] | null;
|
||||
@ -31,7 +32,13 @@ export interface MediaItem {
|
||||
parent_id: string | null;
|
||||
position: number;
|
||||
job?: any;
|
||||
picture_id?: string;
|
||||
responsive?: any; // To hold server-generated responsive data
|
||||
author?: {
|
||||
username: string;
|
||||
display_name: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Mux Video Resolution Types
|
||||
@ -174,6 +181,6 @@ export interface PostNavigationData {
|
||||
comments_count?: number;
|
||||
}[];
|
||||
currentIndex: number;
|
||||
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid';
|
||||
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid' | 'widget';
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user