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" element={<UserProfile />} />
|
||||||
<Route path="/user/:userId/collections" element={<UserCollections />} />
|
<Route path="/user/:userId/collections" element={<UserCollections />} />
|
||||||
<Route path="/user/:userId/pages/new" element={<NewPage />} />
|
<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/new" element={<NewCollection />} />
|
||||||
<Route path="/collections/:userId/:slug" element={<Collections />} />
|
<Route path="/collections/:userId/:slug" element={<Collections />} />
|
||||||
<Route path="/tags/:tag" element={<TagPage />} />
|
<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" element={<UserProfile />} />
|
||||||
<Route path="/org/:orgSlug/user/:userId/collections" element={<UserCollections />} />
|
<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/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/new" element={<NewCollection />} />
|
||||||
<Route path="/org/:orgSlug/collections/:userId/:slug" element={<Collections />} />
|
<Route path="/org/:orgSlug/collections/:userId/:slug" element={<Collections />} />
|
||||||
<Route path="/org/:orgSlug/tags/:tag" element={<TagPage />} />
|
<Route path="/org/:orgSlug/tags/:tag" element={<TagPage />} />
|
||||||
|
|||||||
@ -10,35 +10,16 @@ import { useFeedData } from "@/hooks/useFeedData";
|
|||||||
import { normalizeMediaType, isVideoType } from "@/lib/mediaRegistry";
|
import { normalizeMediaType, isVideoType } from "@/lib/mediaRegistry";
|
||||||
import { UserProfile } from '../pages/Post/types';
|
import { UserProfile } from '../pages/Post/types';
|
||||||
import * as db from '../pages/Post/db';
|
import * as db from '../pages/Post/db';
|
||||||
import type { MediaType } from "@/types";
|
import type { MediaItem, MediaType } from "@/types";
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
|
||||||
// Duplicate types for now or we could reuse specific generic props
|
// 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
|
// 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';
|
import type { FeedSortOption } from '@/hooks/useFeedData';
|
||||||
|
|
||||||
interface GalleryLargeProps {
|
interface GalleryLargeProps {
|
||||||
customPictures?: MediaItemType[];
|
customPictures?: MediaItem[];
|
||||||
customLoading?: boolean;
|
customLoading?: boolean;
|
||||||
navigationSource?: 'home' | 'collection' | 'tag' | 'user';
|
navigationSource?: 'home' | 'collection' | 'tag' | 'user';
|
||||||
navigationSourceId?: string;
|
navigationSourceId?: string;
|
||||||
@ -56,7 +37,7 @@ const GalleryLarge = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setNavigationData, navigationData } = usePostNavigation();
|
const { setNavigationData, navigationData } = usePostNavigation();
|
||||||
const { orgSlug, isOrgContext } = useOrganization();
|
const { orgSlug, isOrgContext } = useOrganization();
|
||||||
const [mediaItems, setMediaItems] = useState<MediaItemType[]>([]);
|
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
|
||||||
const [userLikes, setUserLikes] = useState<Set<string>>(new Set());
|
const [userLikes, setUserLikes] = useState<Set<string>>(new Set());
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@ -73,7 +54,7 @@ const GalleryLarge = ({
|
|||||||
|
|
||||||
// 2. State & Effects
|
// 2. State & Effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let finalMedia: MediaItemType[] = [];
|
let finalMedia: MediaItem[] = [];
|
||||||
|
|
||||||
if (customPictures) {
|
if (customPictures) {
|
||||||
finalMedia = customPictures;
|
finalMedia = customPictures;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { supabase } from '@/integrations/supabase/client';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { translate } from '@/i18n';
|
import { translate } from '@/i18n';
|
||||||
import { QuickAction } from '@/constants';
|
import { QuickAction } from '@/constants';
|
||||||
import { PromptPreset } from '@/components/PresetManager';
|
import { PromptPreset } from '@/components/PresetManager';
|
||||||
import { Workflow } from '@/components/WorkflowManager';
|
import { Workflow } from '@/components/WorkflowManager';
|
||||||
|
import { getUserSettings, updateUserSettings } from '@/lib/db';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings Handlers
|
* Settings Handlers
|
||||||
@ -17,20 +17,13 @@ import { Workflow } from '@/components/WorkflowManager';
|
|||||||
|
|
||||||
export const loadPromptTemplates = async (
|
export const loadPromptTemplates = async (
|
||||||
userId: string,
|
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: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
) => {
|
) => {
|
||||||
setLoadingTemplates(true);
|
setLoadingTemplates(true);
|
||||||
|
console.log('loading prompt templates');
|
||||||
try {
|
try {
|
||||||
const { data: profile, error } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') throw error;
|
|
||||||
|
|
||||||
const settings = profile?.settings as any;
|
|
||||||
const templates = settings?.promptTemplates || [];
|
const templates = settings?.promptTemplates || [];
|
||||||
setPromptTemplates(templates);
|
setPromptTemplates(templates);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -42,29 +35,16 @@ export const loadPromptTemplates = async (
|
|||||||
|
|
||||||
export const savePromptTemplates = async (
|
export const savePromptTemplates = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
templates: Array<{name: string; template: string}>
|
templates: Array<{ name: string; template: string }>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const currentSettings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
|
||||||
|
|
||||||
const currentSettings = (profile?.settings as any) || {};
|
|
||||||
const newSettings = {
|
const newSettings = {
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
promptTemplates: templates
|
promptTemplates: templates
|
||||||
};
|
};
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, newSettings);
|
||||||
.from('profiles')
|
|
||||||
.update({ settings: newSettings as any })
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
toast.success(translate('Templates saved successfully!'));
|
toast.success(translate('Templates saved successfully!'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -81,15 +61,7 @@ export const loadPromptPresets = async (
|
|||||||
) => {
|
) => {
|
||||||
setLoadingPresets(true);
|
setLoadingPresets(true);
|
||||||
try {
|
try {
|
||||||
const { data: profile, error } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') throw error;
|
|
||||||
|
|
||||||
const settings = profile?.settings as any;
|
|
||||||
const presets = settings?.promptPresets || [];
|
const presets = settings?.promptPresets || [];
|
||||||
setPromptPresets(presets);
|
setPromptPresets(presets);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -106,15 +78,7 @@ export const loadWorkflows = async (
|
|||||||
) => {
|
) => {
|
||||||
setLoadingWorkflows(true);
|
setLoadingWorkflows(true);
|
||||||
try {
|
try {
|
||||||
const { data: profile, error } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') throw error;
|
|
||||||
|
|
||||||
const settings = profile?.settings as any;
|
|
||||||
const workflows = settings?.workflows || [];
|
const workflows = settings?.workflows || [];
|
||||||
setWorkflows(workflows);
|
setWorkflows(workflows);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -132,15 +96,7 @@ export const loadQuickActions = async (
|
|||||||
) => {
|
) => {
|
||||||
setLoadingActions(true);
|
setLoadingActions(true);
|
||||||
try {
|
try {
|
||||||
const { data: profile, error } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') throw error;
|
|
||||||
|
|
||||||
const settings = profile?.settings as any;
|
|
||||||
const actions = settings?.quickActions || DEFAULT_QUICK_ACTIONS;
|
const actions = settings?.quickActions || DEFAULT_QUICK_ACTIONS;
|
||||||
setQuickActions(actions);
|
setQuickActions(actions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -156,27 +112,12 @@ export const saveQuickActions = async (
|
|||||||
actions: QuickAction[]
|
actions: QuickAction[]
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const currentSettings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
await updateUserSettings(userId, {
|
||||||
|
|
||||||
const currentSettings = (profile?.settings as any) || {};
|
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
quickActions: actions
|
quickActions: actions
|
||||||
} as any
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
toast.success('Quick Actions saved');
|
toast.success('Quick Actions saved');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -191,15 +132,7 @@ export const loadPromptHistory = async (
|
|||||||
setPromptHistory: React.Dispatch<React.SetStateAction<string[]>>
|
setPromptHistory: React.Dispatch<React.SetStateAction<string[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') throw error;
|
|
||||||
|
|
||||||
const settings = profile?.settings as any;
|
|
||||||
const history = settings?.promptHistory || [];
|
const history = settings?.promptHistory || [];
|
||||||
setPromptHistory(history);
|
setPromptHistory(history);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -216,31 +149,16 @@ export const addToPromptHistory = async (
|
|||||||
if (!promptText.trim()) return;
|
if (!promptText.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const currentSettings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
|
||||||
|
|
||||||
const currentSettings = (profile?.settings as any) || {};
|
|
||||||
const currentHistory = currentSettings.promptHistory || [];
|
const currentHistory = currentSettings.promptHistory || [];
|
||||||
|
|
||||||
// Add to history, avoid duplicates, limit to 50
|
// Add to history, avoid duplicates, limit to 50
|
||||||
const newHistory = [promptText, ...currentHistory.filter((h: string) => h !== promptText)].slice(0, 50);
|
const newHistory = [promptText, ...currentHistory.filter((h: string) => h !== promptText)].slice(0, 50);
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
promptHistory: newHistory
|
promptHistory: newHistory
|
||||||
} as any
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setPromptHistory(newHistory);
|
setPromptHistory(newHistory);
|
||||||
setHistoryIndex(-1); // Reset to latest
|
setHistoryIndex(-1); // Reset to latest
|
||||||
@ -287,15 +205,7 @@ export const savePromptPreset = async (
|
|||||||
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
|
||||||
|
|
||||||
const settings = (profile?.settings as any) || {};
|
|
||||||
const existingPresets = settings.promptPresets || [];
|
const existingPresets = settings.promptPresets || [];
|
||||||
|
|
||||||
const newPreset: PromptPreset = {
|
const newPreset: PromptPreset = {
|
||||||
@ -307,17 +217,10 @@ export const savePromptPreset = async (
|
|||||||
|
|
||||||
const updatedPresets = [...existingPresets, newPreset];
|
const updatedPresets = [...existingPresets, newPreset];
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...settings,
|
...settings,
|
||||||
promptPresets: updatedPresets,
|
promptPresets: updatedPresets,
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setPromptPresets(updatedPresets);
|
setPromptPresets(updatedPresets);
|
||||||
toast.success(translate('Preset saved successfully!'));
|
toast.success(translate('Preset saved successfully!'));
|
||||||
@ -335,15 +238,7 @@ export const updatePromptPreset = async (
|
|||||||
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError) throw fetchError;
|
|
||||||
|
|
||||||
const settings = (profile?.settings as any) || {};
|
|
||||||
const existingPresets = settings.promptPresets || [];
|
const existingPresets = settings.promptPresets || [];
|
||||||
|
|
||||||
const updatedPresets = existingPresets.map((p: PromptPreset) =>
|
const updatedPresets = existingPresets.map((p: PromptPreset) =>
|
||||||
@ -352,17 +247,10 @@ export const updatePromptPreset = async (
|
|||||||
: p
|
: p
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...settings,
|
...settings,
|
||||||
promptPresets: updatedPresets,
|
promptPresets: updatedPresets,
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setPromptPresets(updatedPresets);
|
setPromptPresets(updatedPresets);
|
||||||
toast.success(translate('Preset updated successfully!'));
|
toast.success(translate('Preset updated successfully!'));
|
||||||
@ -379,30 +267,15 @@ export const deletePromptPreset = async (
|
|||||||
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
setPromptPresets: React.Dispatch<React.SetStateAction<PromptPreset[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError) throw fetchError;
|
|
||||||
|
|
||||||
const settings = (profile?.settings as any) || {};
|
|
||||||
const existingPresets = settings.promptPresets || [];
|
const existingPresets = settings.promptPresets || [];
|
||||||
|
|
||||||
const updatedPresets = existingPresets.filter((p: PromptPreset) => p.id !== id);
|
const updatedPresets = existingPresets.filter((p: PromptPreset) => p.id !== id);
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...settings,
|
...settings,
|
||||||
promptPresets: updatedPresets,
|
promptPresets: updatedPresets,
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setPromptPresets(updatedPresets);
|
setPromptPresets(updatedPresets);
|
||||||
toast.success(translate('Preset deleted successfully!'));
|
toast.success(translate('Preset deleted successfully!'));
|
||||||
@ -423,15 +296,7 @@ export const saveWorkflow = async (
|
|||||||
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError && fetchError.code !== 'PGRST116') throw fetchError;
|
|
||||||
|
|
||||||
const settings = (profile?.settings as any) || {};
|
|
||||||
const existingWorkflows = settings.workflows || [];
|
const existingWorkflows = settings.workflows || [];
|
||||||
|
|
||||||
const newWorkflow: Workflow = {
|
const newWorkflow: Workflow = {
|
||||||
@ -443,17 +308,10 @@ export const saveWorkflow = async (
|
|||||||
|
|
||||||
const updatedWorkflows = [...existingWorkflows, newWorkflow];
|
const updatedWorkflows = [...existingWorkflows, newWorkflow];
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...settings,
|
...settings,
|
||||||
workflows: updatedWorkflows,
|
workflows: updatedWorkflows,
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setWorkflows(updatedWorkflows);
|
setWorkflows(updatedWorkflows);
|
||||||
toast.success(translate('Workflow saved successfully!'));
|
toast.success(translate('Workflow saved successfully!'));
|
||||||
@ -471,15 +329,7 @@ export const updateWorkflow = async (
|
|||||||
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError) throw fetchError;
|
|
||||||
|
|
||||||
const settings = (profile?.settings as any) || {};
|
|
||||||
const existingWorkflows = settings.workflows || [];
|
const existingWorkflows = settings.workflows || [];
|
||||||
|
|
||||||
const updatedWorkflows = existingWorkflows.map((w: Workflow) =>
|
const updatedWorkflows = existingWorkflows.map((w: Workflow) =>
|
||||||
@ -488,17 +338,10 @@ export const updateWorkflow = async (
|
|||||||
: w
|
: w
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...settings,
|
...settings,
|
||||||
workflows: updatedWorkflows,
|
workflows: updatedWorkflows,
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setWorkflows(updatedWorkflows);
|
setWorkflows(updatedWorkflows);
|
||||||
toast.success(translate('Workflow updated successfully!'));
|
toast.success(translate('Workflow updated successfully!'));
|
||||||
@ -515,30 +358,15 @@ export const deleteWorkflow = async (
|
|||||||
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
setWorkflows: React.Dispatch<React.SetStateAction<Workflow[]>>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: fetchError } = await supabase
|
const settings = await getUserSettings(userId);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError) throw fetchError;
|
|
||||||
|
|
||||||
const settings = (profile?.settings as any) || {};
|
|
||||||
const existingWorkflows = settings.workflows || [];
|
const existingWorkflows = settings.workflows || [];
|
||||||
|
|
||||||
const updatedWorkflows = existingWorkflows.filter((w: Workflow) => w.id !== id);
|
const updatedWorkflows = existingWorkflows.filter((w: Workflow) => w.id !== id);
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
await updateUserSettings(userId, {
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
settings: {
|
|
||||||
...settings,
|
...settings,
|
||||||
workflows: updatedWorkflows,
|
workflows: updatedWorkflows,
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setWorkflows(updatedWorkflows);
|
setWorkflows(updatedWorkflows);
|
||||||
toast.success(translate('Workflow deleted successfully!'));
|
toast.success(translate('Workflow deleted successfully!'));
|
||||||
|
|||||||
@ -34,6 +34,7 @@ interface MediaCardProps {
|
|||||||
job?: any;
|
job?: any;
|
||||||
variant?: 'grid' | 'feed';
|
variant?: 'grid' | 'feed';
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
|
versionCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaCard: React.FC<MediaCardProps> = ({
|
const MediaCard: React.FC<MediaCardProps> = ({
|
||||||
@ -60,7 +61,8 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
|||||||
responsive,
|
responsive,
|
||||||
job,
|
job,
|
||||||
variant = 'grid',
|
variant = 'grid',
|
||||||
apiUrl
|
apiUrl,
|
||||||
|
versionCount
|
||||||
}) => {
|
}) => {
|
||||||
const normalizedType = normalizeMediaType(type);
|
const normalizedType = normalizeMediaType(type);
|
||||||
// Render based on type
|
// Render based on type
|
||||||
@ -151,6 +153,7 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
|||||||
responsive={responsive}
|
responsive={responsive}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
apiUrl={apiUrl}
|
apiUrl={apiUrl}
|
||||||
|
versionCount={versionCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ interface PageCardProps extends Omit<MediaRendererProps, 'created_at'> {
|
|||||||
authorAvatarUrl?: string | null;
|
authorAvatarUrl?: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
|
versionCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageCard: React.FC<PageCardProps> = ({
|
const PageCard: React.FC<PageCardProps> = ({
|
||||||
@ -34,11 +35,13 @@ const PageCard: React.FC<PageCardProps> = ({
|
|||||||
variant = 'grid',
|
variant = 'grid',
|
||||||
responsive,
|
responsive,
|
||||||
showContent = true,
|
showContent = true,
|
||||||
apiUrl
|
apiUrl,
|
||||||
|
versionCount
|
||||||
}) => {
|
}) => {
|
||||||
// Determine image source
|
// Determine image source
|
||||||
// If url is missing or empty, fallback to picsum
|
// If url is missing or empty, fallback to picsum
|
||||||
// For PAGE_EXTERNAL, currently 'url' is the link and 'thumbnailUrl' is the image.
|
// For PAGE_EXTERNAL, currently 'url' is the link and 'thumbnailUrl' is the image.
|
||||||
|
|
||||||
const displayImage = thumbnailUrl || url || "https://picsum.photos/640";
|
const displayImage = thumbnailUrl || url || "https://picsum.photos/640";
|
||||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||||
|
|
||||||
@ -47,7 +50,6 @@ const PageCard: React.FC<PageCardProps> = ({
|
|||||||
const ytId = getYouTubeVideoId(url);
|
const ytId = getYouTubeVideoId(url);
|
||||||
const isExternalVideo = !!(tikTokId || ytId);
|
const isExternalVideo = !!(tikTokId || ytId);
|
||||||
|
|
||||||
|
|
||||||
// Use thumbnail if available and preferred (logic from MediaCard usually handles this before passing url,
|
// Use thumbnail if available and preferred (logic from MediaCard usually handles this before passing url,
|
||||||
// but here we ensure we have *something*).
|
// but here we ensure we have *something*).
|
||||||
|
|
||||||
|
|||||||
@ -41,11 +41,12 @@ interface Page {
|
|||||||
|
|
||||||
interface PageManagerProps {
|
interface PageManagerProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
username?: string;
|
||||||
isOwnProfile: boolean;
|
isOwnProfile: boolean;
|
||||||
orgSlug?: string;
|
orgSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
const PageManager = ({ userId, username, isOwnProfile, orgSlug }: PageManagerProps) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [pages, setPages] = useState<Page[]>([]);
|
const [pages, setPages] = useState<Page[]>([]);
|
||||||
@ -152,9 +153,9 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
|||||||
|
|
||||||
const getPageUrl = (slug: string) => {
|
const getPageUrl = (slug: string) => {
|
||||||
if (orgSlug) {
|
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) {
|
if (loading) {
|
||||||
@ -179,8 +180,8 @@ const PageManager = ({ userId, isOwnProfile, orgSlug }: PageManagerProps) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPageUrl = orgSlug
|
const newPageUrl = orgSlug
|
||||||
? `/org/${orgSlug}/user/${userId}/pages/new`
|
? `/org/${orgSlug}/user/${username || userId}/pages/new`
|
||||||
: `/user/${userId}/pages/new`;
|
: `/user/${username || userId}/pages/new`;
|
||||||
navigate(newPageUrl);
|
navigate(newPageUrl);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ interface PhotoCardProps {
|
|||||||
responsive?: any;
|
responsive?: any;
|
||||||
variant?: 'grid' | 'feed';
|
variant?: 'grid' | 'feed';
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
|
versionCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhotoCard = ({
|
const PhotoCard = ({
|
||||||
@ -60,7 +61,8 @@ const PhotoCard = ({
|
|||||||
showContent = true,
|
showContent = true,
|
||||||
responsive,
|
responsive,
|
||||||
variant = 'grid',
|
variant = 'grid',
|
||||||
apiUrl
|
apiUrl,
|
||||||
|
versionCount
|
||||||
}: PhotoCardProps) => {
|
}: PhotoCardProps) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -72,48 +74,28 @@ const PhotoCard = ({
|
|||||||
const [showLightbox, setShowLightbox] = useState(false);
|
const [showLightbox, setShowLightbox] = useState(false);
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [isPublishing, setIsPublishing] = useState(false);
|
const [isPublishing, setIsPublishing] = useState(false);
|
||||||
|
|
||||||
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | null>(null);
|
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | null>(null);
|
||||||
const [versionCount, setVersionCount] = useState<number>(0);
|
|
||||||
|
|
||||||
const isOwner = user?.id === authorId;
|
const isOwner = user?.id === authorId;
|
||||||
|
|
||||||
// Fetch version count for owners only
|
// 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(() => {
|
useEffect(() => {
|
||||||
const fetchVersionCount = async () => {
|
if (versionCount !== undefined) {
|
||||||
if (!isOwner || !user) return;
|
setLocalVersionCount(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}`);
|
|
||||||
}
|
}
|
||||||
|
}, [versionCount]);
|
||||||
|
|
||||||
const { count } = await query;
|
// Legacy fetch removed in favor of passed prop
|
||||||
// console.log('Version count:', count);
|
/*
|
||||||
setVersionCount(count || 1);
|
useEffect(() => {
|
||||||
} catch (error) {
|
// ... legacy fetch logic ...
|
||||||
console.error('Error fetching version count:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchVersionCount();
|
|
||||||
}, [pictureId, isOwner, user]);
|
}, [pictureId, isOwner, user]);
|
||||||
|
*/
|
||||||
|
|
||||||
const handleLike = async (e: React.MouseEvent) => {
|
const handleLike = async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -488,10 +470,10 @@ const PhotoCard = ({
|
|||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{versionCount > 1 && (
|
{localVersionCount > 1 && (
|
||||||
<div className="flex items-center ml-2 px-2 py-1 bg-white/20 rounded text-white text-xs">
|
<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" />
|
<Layers className="h-3 w-3 mr-1" />
|
||||||
{versionCount}
|
{localVersionCount}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -31,9 +31,10 @@ export interface MediaItemType {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
comments: { count: number }[];
|
comments: { count: number }[];
|
||||||
|
|
||||||
author_profile?: UserProfile;
|
author?: UserProfile;
|
||||||
job?: any;
|
job?: any;
|
||||||
responsive?: any; // Add responsive data
|
responsive?: any; // Add responsive data
|
||||||
|
versionCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { FeedSortOption } from '@/hooks/useFeedData';
|
import type { FeedSortOption } from '@/hooks/useFeedData';
|
||||||
@ -98,7 +99,6 @@ const MediaGrid = ({
|
|||||||
enabled: !customPictures,
|
enabled: !customPictures,
|
||||||
supabaseClient
|
supabaseClient
|
||||||
});
|
});
|
||||||
|
|
||||||
// Infinite Scroll Observer
|
// Infinite Scroll Observer
|
||||||
const observerTarget = useRef(null);
|
const observerTarget = useRef(null);
|
||||||
|
|
||||||
@ -182,6 +182,8 @@ const MediaGrid = ({
|
|||||||
hasRestoredScroll.current = false;
|
hasRestoredScroll.current = false;
|
||||||
}, [cacheKey]);
|
}, [cacheKey]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Track scroll position
|
// Track scroll position
|
||||||
const lastScrollY = useRef(window.scrollY);
|
const lastScrollY = useRef(window.scrollY);
|
||||||
|
|
||||||
@ -252,8 +254,9 @@ const MediaGrid = ({
|
|||||||
// Handle Page navigation
|
// Handle Page navigation
|
||||||
if (type === 'page-intern') {
|
if (type === 'page-intern') {
|
||||||
const item = mediaItems.find(i => i.id === mediaId);
|
const item = mediaItems.find(i => i.id === mediaId);
|
||||||
|
|
||||||
if (item && item.meta?.slug) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -382,7 +385,6 @@ const MediaGrid = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasItems = mediaItems.length > 0;
|
const hasItems = mediaItems.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full relative">
|
<div className="w-full relative">
|
||||||
{hasItems && isOwner && onFilesDrop && navigationSource === 'collection' && (
|
{hasItems && isOwner && onFilesDrop && navigationSource === 'collection' && (
|
||||||
|
|||||||
@ -16,12 +16,14 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { T, getCurrentLang, supportedLanguages, translate, setLanguage } from "@/i18n";
|
||||||
import { CreationWizardPopup } from './CreationWizardPopup';
|
import { CreationWizardPopup } from './CreationWizardPopup';
|
||||||
|
|
||||||
const TopNavigation = () => {
|
const TopNavigation = () => {
|
||||||
const { user, signOut, roles } = useAuth();
|
const { user, signOut, roles } = useAuth();
|
||||||
|
const { fetchProfile, profiles } = useProfiles();
|
||||||
const { orgSlug, isOrgContext } = useOrganization();
|
const { orgSlug, isOrgContext } = useOrganization();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -33,8 +35,43 @@ const TopNavigation = () => {
|
|||||||
|
|
||||||
const authPath = isOrgContext ? `/org/${orgSlug}/auth` : '/auth';
|
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;
|
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) => {
|
const handleLanguageChange = (langCode: string) => {
|
||||||
setLanguage(langCode as any);
|
setLanguage(langCode as any);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -50,7 +50,13 @@ const UserAvatarBlock: React.FC<UserAvatarBlockProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
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
|
const nameClass = hoverStyle
|
||||||
|
|||||||
@ -24,17 +24,20 @@ export const FeedCard: React.FC<FeedCardProps> = ({
|
|||||||
onNavigate
|
onNavigate
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
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 [likeCount, setLikeCount] = useState(post.likes_count || 0);
|
||||||
const [lastTap, setLastTap] = useState<number>(0);
|
const [lastTap, setLastTap] = useState<number>(0);
|
||||||
const [showHeartAnimation, setShowHeartAnimation] = useState(false);
|
const [showHeartAnimation, setShowHeartAnimation] = useState(false);
|
||||||
|
|
||||||
// Initial check for like status (you might want to pass this in from parent if checking many)
|
// Initial check removal: We now rely on server-provided `is_liked` status.
|
||||||
React.useEffect(() => {
|
// React.useEffect(() => {
|
||||||
if (currentUserId && post.cover?.id) {
|
// if (currentUserId && post.cover?.id) {
|
||||||
db.checkLikeStatus(currentUserId, post.cover.id).then(setIsLiked);
|
// db.checkLikeStatus(currentUserId, post.cover.id).then(setIsLiked);
|
||||||
}
|
// }
|
||||||
}, [currentUserId, post.cover?.id]);
|
// }, [currentUserId, post.cover?.id]);
|
||||||
|
|
||||||
const handleLike = async () => {
|
const handleLike = async () => {
|
||||||
if (!currentUserId || !post.cover?.id) return;
|
if (!currentUserId || !post.cover?.id) return;
|
||||||
@ -79,7 +82,8 @@ export const FeedCard: React.FC<FeedCardProps> = ({
|
|||||||
if (item) {
|
if (item) {
|
||||||
const type = normalizeMediaType(item.type);
|
const type = normalizeMediaType(item.type);
|
||||||
if (type === 'page-intern' && item.meta?.slug) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,9 +103,9 @@ export const FeedCard: React.FC<FeedCardProps> = ({
|
|||||||
items={carouselItems}
|
items={carouselItems}
|
||||||
aspectRatio={1}
|
aspectRatio={1}
|
||||||
className="w-full bg-muted"
|
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}
|
authorId={post.user_id}
|
||||||
authorAvatarUrl={post.author_profile?.avatar_url}
|
authorAvatarUrl={post.author?.avatar_url}
|
||||||
onItemClick={handleItemClick}
|
onItemClick={handleItemClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ interface GenericCanvasProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
selectedWidgetId?: string | null;
|
selectedWidgetId?: string | null;
|
||||||
onSelectWidget?: (widgetId: string) => void;
|
onSelectWidget?: (widgetId: string) => void;
|
||||||
|
initialLayout?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
||||||
@ -24,11 +25,13 @@ const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
|||||||
showControls = true,
|
showControls = true,
|
||||||
className = '',
|
className = '',
|
||||||
selectedWidgetId,
|
selectedWidgetId,
|
||||||
onSelectWidget
|
onSelectWidget,
|
||||||
|
initialLayout
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadedPages,
|
loadedPages,
|
||||||
loadPageLayout,
|
loadPageLayout,
|
||||||
|
hydratePageLayout,
|
||||||
addWidgetToPage,
|
addWidgetToPage,
|
||||||
removeWidgetFromPage,
|
removeWidgetFromPage,
|
||||||
moveWidgetInPage,
|
moveWidgetInPage,
|
||||||
@ -45,11 +48,17 @@ const GenericCanvasComponent: React.FC<GenericCanvasProps> = ({
|
|||||||
const layout = loadedPages.get(pageId);
|
const layout = loadedPages.get(pageId);
|
||||||
|
|
||||||
// Load the page layout on mount
|
// Load the page layout on mount
|
||||||
|
// Load the page layout on mount or hydrate from prop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (initialLayout && !layout) {
|
||||||
|
hydratePageLayout(pageId, initialLayout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!layout) {
|
if (!layout) {
|
||||||
loadPageLayout(pageId, pageName);
|
loadPageLayout(pageId, pageName);
|
||||||
}
|
}
|
||||||
}, [pageId, pageName, layout, loadPageLayout]);
|
}, [pageId, pageName, layout, loadPageLayout, hydratePageLayout, initialLayout]);
|
||||||
|
|
||||||
const [selectedContainer, setSelectedContainer] = useState<string | null>(null);
|
const [selectedContainer, setSelectedContainer] = useState<string | null>(null);
|
||||||
const [showWidgetPalette, setShowWidgetPalette] = useState(false);
|
const [showWidgetPalette, setShowWidgetPalette] = useState(false);
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { FilterPanel } from '@/components/filters/FilterPanel';
|
import { FilterPanel } from '@/components/filters/FilterPanel';
|
||||||
import AITextGenerator from '@/components/AITextGenerator';
|
import AITextGenerator from '@/components/AITextGenerator';
|
||||||
import { getUserSecrets } from '@/components/ImageWizard/db';
|
import { getUserSecrets } from '@/components/ImageWizard/db';
|
||||||
|
import * as db from '@/lib/db';
|
||||||
|
|
||||||
interface MarkdownTextWidgetProps {
|
interface MarkdownTextWidgetProps {
|
||||||
isEditMode?: boolean;
|
isEditMode?: boolean;
|
||||||
@ -85,24 +86,17 @@ const MarkdownTextWidget: React.FC<MarkdownTextWidgetProps> = ({
|
|||||||
// Load AI Text Generator settings from profile
|
// Load AI Text Generator settings from profile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSettings = async () => {
|
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);
|
setSettingsLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: profile, error } = await supabase
|
// Use the centralized cached fetcher instead of direct call
|
||||||
.from('profiles')
|
// This handles deduplication if multiple widgets load at once
|
||||||
.select('settings')
|
const settings = await db.getUserSettings(user.id);
|
||||||
.eq('user_id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
setSettingsLoaded(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = profile?.settings as any;
|
|
||||||
const aiTextSettings = settings?.aiTextGenerator;
|
const aiTextSettings = settings?.aiTextGenerator;
|
||||||
|
|
||||||
if (aiTextSettings) {
|
if (aiTextSettings) {
|
||||||
@ -125,7 +119,7 @@ const MarkdownTextWidget: React.FC<MarkdownTextWidgetProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}, [user]);
|
}, [user, isEditMode]);
|
||||||
|
|
||||||
// Save AI Text Generator settings to profile (debounced)
|
// Save AI Text Generator settings to profile (debounced)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -133,13 +127,7 @@ const MarkdownTextWidget: React.FC<MarkdownTextWidgetProps> = ({
|
|||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const { data: profile } = await supabase
|
const currentSettings = await db.getUserSettings(user.id);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
const currentSettings = (profile?.settings as any) || {};
|
|
||||||
|
|
||||||
const updatedSettings = {
|
const updatedSettings = {
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
@ -153,14 +141,7 @@ const MarkdownTextWidget: React.FC<MarkdownTextWidgetProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { error } = await supabase
|
await db.updateUserSettings(user.id, updatedSettings);
|
||||||
.from('profiles')
|
|
||||||
.update({ settings: updatedSettings })
|
|
||||||
.eq('user_id', user.id);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to save AITextGenerator settings:', error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving AITextGenerator settings:', error);
|
console.error('Error saving AITextGenerator settings:', error);
|
||||||
}
|
}
|
||||||
@ -242,15 +223,9 @@ const MarkdownTextWidget: React.FC<MarkdownTextWidgetProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: userProvider, error } = await supabase
|
const userProvider = await db.getProviderConfig(user.id, provider);
|
||||||
.from('provider_configs')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.eq('name', provider)
|
|
||||||
.eq('is_active', true)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error || !userProvider) {
|
if (!userProvider) {
|
||||||
// Only warn if checks in user_secrets also failed or if it's a different provider
|
// Only warn if checks in user_secrets also failed or if it's a different provider
|
||||||
if (provider !== 'openai') {
|
if (provider !== 'openai') {
|
||||||
console.warn(`No provider configuration found for ${provider}`);
|
console.warn(`No provider configuration found for ${provider}`);
|
||||||
|
|||||||
@ -21,6 +21,7 @@ interface LayoutContextType {
|
|||||||
renameWidget: (pageId: string, widgetInstanceId: string, newId: string) => Promise<boolean>;
|
renameWidget: (pageId: string, widgetInstanceId: string, newId: string) => Promise<boolean>;
|
||||||
exportPageLayout: (pageId: string) => Promise<string>;
|
exportPageLayout: (pageId: string) => Promise<string>;
|
||||||
importPageLayout: (pageId: string, jsonData: string) => Promise<PageLayout>;
|
importPageLayout: (pageId: string, jsonData: string) => Promise<PageLayout>;
|
||||||
|
hydratePageLayout: (pageId: string, layout: PageLayout) => void;
|
||||||
|
|
||||||
// Manual save
|
// Manual save
|
||||||
saveToApi: () => Promise<boolean>;
|
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> => {
|
const saveToApi = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
// Save all loaded pages to database
|
// Save all loaded pages to database
|
||||||
@ -417,6 +426,7 @@ export const LayoutProvider: React.FC<LayoutProviderProps> = ({ children }) => {
|
|||||||
updateWidgetProps,
|
updateWidgetProps,
|
||||||
exportPageLayout,
|
exportPageLayout,
|
||||||
importPageLayout,
|
importPageLayout,
|
||||||
|
hydratePageLayout,
|
||||||
saveToApi,
|
saveToApi,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadedPages,
|
loadedPages,
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
|
||||||
import { UserProfile } from "@/pages/Post/types";
|
import { UserProfile } from "@/pages/Post/types";
|
||||||
|
|
||||||
interface ProfilesContextType {
|
interface ProfilesContextType {
|
||||||
@ -62,7 +61,6 @@ export const ProfilesProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
|
|
||||||
const fetchProfile = useCallback(async (userId: string) => {
|
const fetchProfile = useCallback(async (userId: string) => {
|
||||||
if (profiles[userId]) return profiles[userId];
|
if (profiles[userId]) return profiles[userId];
|
||||||
|
|
||||||
await fetchProfiles([userId]);
|
await fetchProfiles([userId]);
|
||||||
return profiles[userId] || null;
|
return profiles[userId] || null;
|
||||||
}, [profiles, fetchProfiles]);
|
}, [profiles, fetchProfiles]);
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { FEED_API_ENDPOINT, FEED_PAGE_SIZE } from '@/constants';
|
|||||||
import { useProfiles } from '@/contexts/ProfilesContext';
|
import { useProfiles } from '@/contexts/ProfilesContext';
|
||||||
import { useFeedCache } from '@/contexts/FeedCacheContext';
|
import { useFeedCache } from '@/contexts/FeedCacheContext';
|
||||||
|
|
||||||
|
const { supabase } = await import('@/integrations/supabase/client');
|
||||||
|
|
||||||
export type FeedSortOption = 'latest' | 'top';
|
export type FeedSortOption = 'latest' | 'top';
|
||||||
|
|
||||||
interface UseFeedDataProps {
|
interface UseFeedDataProps {
|
||||||
@ -118,30 +120,54 @@ export const useFeedData = ({
|
|||||||
|
|
||||||
console.log('Hydrated feed', fetchedPosts);
|
console.log('Hydrated feed', fetchedPosts);
|
||||||
}
|
}
|
||||||
// 2. API Fetch (Home only)
|
// 2. API Fetch (Universal)
|
||||||
else if (source === 'home' && !sourceId) {
|
// 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 SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
|
||||||
const fetchUrl = SERVER_URL
|
let queryParams = `?page=${currentPage}&limit=${FEED_PAGE_SIZE}&sortBy=${sortBy}`;
|
||||||
? `${SERVER_URL}${FEED_API_ENDPOINT}?page=${currentPage}&limit=${FEED_PAGE_SIZE}`
|
if (source) queryParams += `&source=${source}`;
|
||||||
: `${FEED_API_ENDPOINT}?page=${currentPage}&limit=${FEED_PAGE_SIZE}`;
|
if (sourceId) queryParams += `&sourceId=${sourceId}`;
|
||||||
|
|
||||||
const res = await fetch(fetchUrl);
|
// If we have token, pass it?
|
||||||
if (!res.ok) throw new Error(`Feed fetch failed: ${res.statusText}`);
|
// 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();
|
fetchedPosts = await res.json();
|
||||||
}
|
}
|
||||||
// 3. Fallback DB Fetch
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
// 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 {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (fetchedPosts.length < FEED_PAGE_SIZE) {
|
if (fetchedPosts.length < FEED_PAGE_SIZE) {
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { supabase } from '@/integrations/supabase/client';
|
|||||||
import { useLog } from '@/contexts/LogContext';
|
import { useLog } from '@/contexts/LogContext';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { translate } from '@/i18n';
|
import { translate } from '@/i18n';
|
||||||
|
import { getUserSettings, updateUserSettings } from '@/lib/db';
|
||||||
|
|
||||||
const MAX_HISTORY_LENGTH = 50;
|
const MAX_HISTORY_LENGTH = 50;
|
||||||
|
|
||||||
@ -18,17 +19,9 @@ export const usePromptHistory = () => {
|
|||||||
const fetchHistory = async () => {
|
const fetchHistory = async () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const settings = await getUserSettings(user.id);
|
||||||
.from('profiles')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
const settings = data?.settings;
|
|
||||||
const history = (settings && typeof settings === 'object' && 'promptHistory' in settings && Array.isArray(settings.promptHistory))
|
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);
|
setPromptHistory(history);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -43,26 +36,13 @@ export const usePromptHistory = () => {
|
|||||||
if (user) {
|
if (user) {
|
||||||
try {
|
try {
|
||||||
// First, fetch the current settings to avoid overwriting them
|
// First, fetch the current settings to avoid overwriting them
|
||||||
const { data: profileData, error: fetchError } = await supabase
|
const currentSettings = await getUserSettings(user.id);
|
||||||
.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 newSettings = {
|
const newSettings = {
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
promptHistory: newHistory,
|
promptHistory: newHistory,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { error } = await supabase
|
await updateUserSettings(user.id, newSettings);
|
||||||
.from('profiles')
|
|
||||||
.update({ settings: newSettings })
|
|
||||||
.eq('user_id', user.id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
addLog('debug', '[PromptHistory] Saved updated prompt history to Supabase');
|
addLog('debug', '[PromptHistory] Saved updated prompt history to Supabase');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
addLog('error', '[PromptHistory] Failed to save history', error);
|
addLog('error', '[PromptHistory] Failed to save history', error);
|
||||||
|
|||||||
@ -35,6 +35,8 @@ interface ImageResult {
|
|||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { getUserGoogleApiKey } from '@/lib/db';
|
||||||
|
|
||||||
// Get user's Google API key from user_secrets
|
// Get user's Google API key from user_secrets
|
||||||
export const getGoogleApiKey = async (): Promise<string | null> => {
|
export const getGoogleApiKey = async (): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
@ -44,19 +46,7 @@ export const getGoogleApiKey = async (): Promise<string | null> => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: secretData, error } = await supabase
|
const apiKey = await getUserGoogleApiKey(user.id);
|
||||||
.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;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
logger.error('No Google API key found in user secrets. Please add your Google API key in your profile settings.');
|
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 { MediaType, MediaItem } from "@/types";
|
||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
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 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)) {
|
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);
|
requestCache.delete(key);
|
||||||
throw err;
|
throw err;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
// Clear cache after a short delay to allow immediate re-renders to share data
|
// Clear memory cache after timeout to allow new fetches
|
||||||
// but prevent stale data issues.
|
// For 'local' storage, we technically might not need to clear memory cache as fast,
|
||||||
setTimeout(() => requestCache.delete(key), 2500);
|
// 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);
|
requestCache.set(key, promise);
|
||||||
|
} else {
|
||||||
|
console.debug(`[db] Cache HIT: ${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestCache.get(key) as Promise<T>;
|
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) => {
|
export const fetchPostById = async (id: string, client?: SupabaseClient) => {
|
||||||
const supabase = client || defaultSupabase;
|
const supabase = client || defaultSupabase;
|
||||||
return fetchWithDeduplication(`post-${id}`, async () => {
|
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) => {
|
export const getUserSettings = async (userId: string, client?: SupabaseClient) => {
|
||||||
const supabase = client || defaultSupabase;
|
const supabase = client || defaultSupabase;
|
||||||
|
console.log('getUserSettings', userId);
|
||||||
return fetchWithDeduplication(`settings-${userId}`, async () => {
|
return fetchWithDeduplication(`settings-${userId}`, async () => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
@ -226,7 +308,7 @@ export const getUserSettings = async (userId: string, client?: SupabaseClient) =
|
|||||||
.single();
|
.single();
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return (data?.settings as any) || {};
|
return (data?.settings as any) || {};
|
||||||
});
|
}, 100000);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateUserSettings = async (userId: string, settings: any, client?: SupabaseClient) => {
|
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) => {
|
export const addCollectionPictures = async (inserts: { collection_id: string, picture_id: string }[], client?: SupabaseClient) => {
|
||||||
const supabase = client || defaultSupabase;
|
const supabase = client || defaultSupabase;
|
||||||
const { error } = await supabase
|
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 (
|
export const fetchFeedPosts = async (
|
||||||
source: 'home' | 'collection' | 'tag' | 'user' | 'widget' = 'home',
|
source: 'home' | 'collection' | 'tag' | 'user' | 'widget' = 'home',
|
||||||
sourceId?: string,
|
sourceId?: string,
|
||||||
@ -375,7 +512,6 @@ export const fetchFeedPosts = async (
|
|||||||
orgSlug?: string,
|
orgSlug?: string,
|
||||||
client?: SupabaseClient
|
client?: SupabaseClient
|
||||||
): Promise<FeedPost[]> => {
|
): Promise<FeedPost[]> => {
|
||||||
// Forward to paginated version with defaults
|
|
||||||
return fetchFeedPostsPaginated(source, sourceId, isOrgContext, orgSlug, 0, 30, client);
|
return fetchFeedPostsPaginated(source, sourceId, isOrgContext, orgSlug, 0, 30, client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -435,6 +571,25 @@ export const fetchFeedPostsPaginated = async (
|
|||||||
const { data: postsData, error: postsError } = await query;
|
const { data: postsData, error: postsError } = await query;
|
||||||
if (postsError) throw postsError;
|
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)
|
// 2. Fetch Pages (if applicable)
|
||||||
// Only fetch pages for home/user/org sources to keep it simple for now
|
// Only fetch pages for home/user/org sources to keep it simple for now
|
||||||
let pagesData: any[] = [];
|
let pagesData: any[] = [];
|
||||||
@ -683,6 +838,9 @@ export const mapFeedPostsToMediaItems = (posts: FeedPost[], sortBy: 'latest' | '
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!cover) return null;
|
if (!cover) return null;
|
||||||
|
|
||||||
|
const versionCount = post.pictures ? post.pictures.length : 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
picture_id: cover.id,
|
picture_id: cover.id,
|
||||||
@ -698,12 +856,13 @@ export const mapFeedPostsToMediaItems = (posts: FeedPost[], sortBy: 'latest' | '
|
|||||||
comments: [{ count: post.comments_count }],
|
comments: [{ count: post.comments_count }],
|
||||||
responsive: (cover as any).responsive,
|
responsive: (cover as any).responsive,
|
||||||
job: (cover as any).job,
|
job: (cover as any).job,
|
||||||
// author_profile must be populated externally
|
author: post.author,
|
||||||
|
versionCount
|
||||||
};
|
};
|
||||||
}).filter(item => item !== null);
|
}).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[] => {
|
export const augmentFeedPosts = (posts: any[]): FeedPost[] => {
|
||||||
return posts.map(p => {
|
return posts.map(p => {
|
||||||
// Check if we need to augment (heuristic: missing cover)
|
// Check if we need to augment (heuristic: missing cover)
|
||||||
@ -715,12 +874,12 @@ export const augmentFeedPosts = (posts: any[]): FeedPost[] => {
|
|||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
cover: validPics[0] || pics[0], // fallback to first if none visible?
|
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,
|
user_id: p.author.user_id,
|
||||||
username: p.author.username,
|
username: p.author.username,
|
||||||
display_name: p.author.display_name,
|
display_name: p.author.display_name,
|
||||||
avatar_url: p.author.avatar_url
|
avatar_url: p.author.avatar_url
|
||||||
} : undefined
|
} : undefined)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { JSONSchema } from 'openai/lib/jsonschema';
|
|||||||
import { createImage as createImageRouter, editImage as editImageRouter } from '@/lib/image-router';
|
import { createImage as createImageRouter, editImage as editImageRouter } from '@/lib/image-router';
|
||||||
import { generateTextWithImagesTool } from '@/lib/markdownImageTools';
|
import { generateTextWithImagesTool } from '@/lib/markdownImageTools';
|
||||||
import { createPageTool } from '@/lib/pageTools';
|
import { createPageTool } from '@/lib/pageTools';
|
||||||
|
import { getUserOpenAIKey } from '@/lib/db';
|
||||||
|
|
||||||
type LogFunction = (level: string, message: string, data?: any) => void;
|
type LogFunction = (level: string, message: string, data?: any) => void;
|
||||||
|
|
||||||
@ -80,19 +81,7 @@ const getOpenAIApiKey = async (): Promise<string | null> => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: secretData, error } = await supabase
|
const apiKey = await getUserOpenAIKey(user.id);
|
||||||
.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;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
consoleLogger.error('No OpenAI API key found in user secrets. Please add your OpenAI API key in your profile settings.');
|
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);
|
container.children.forEach(processContainer);
|
||||||
};
|
};
|
||||||
|
|
||||||
layout.containers.forEach(processContainer);
|
layout.containers?.forEach(processContainer);
|
||||||
|
|
||||||
return allHeadings;
|
return allHeadings;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,5 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
|
||||||
import App from "./App.tsx";
|
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 "./index.css";
|
||||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import { toast } from 'sonner';
|
|||||||
import { translate } from '@/i18n';
|
import { translate } from '@/i18n';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import SimpleLogViewer from '@/components/SimpleLogViewer';
|
import SimpleLogViewer from '@/components/SimpleLogViewer';
|
||||||
|
import { getUserSettings, updateUserSettings, getProviderConfig } from '@/lib/db';
|
||||||
|
import { usePromptHistory } from '@/hooks/usePromptHistory';
|
||||||
|
|
||||||
const PlaygroundEditorLLM: React.FC = () => {
|
const PlaygroundEditorLLM: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@ -23,16 +25,26 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
const [selectedText, setSelectedText] = useState('');
|
const [selectedText, setSelectedText] = useState('');
|
||||||
|
|
||||||
// AI Text Generator state
|
// AI Text Generator state
|
||||||
const [prompt, setPrompt] = useState('');
|
// AI Text Generator state
|
||||||
const [promptHistory, setPromptHistory] = useState<string[]>([]);
|
const {
|
||||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
prompt,
|
||||||
|
setPrompt,
|
||||||
|
promptHistory,
|
||||||
|
historyIndex,
|
||||||
|
setHistoryIndex,
|
||||||
|
navigateHistory,
|
||||||
|
addPromptToHistory
|
||||||
|
} = usePromptHistory();
|
||||||
|
|
||||||
const [selectedProvider, setSelectedProvider] = useState('openai');
|
const [selectedProvider, setSelectedProvider] = useState('openai');
|
||||||
const [selectedModel, setSelectedModel] = useState('gpt-5');
|
const [selectedModel, setSelectedModel] = useState('gpt-5');
|
||||||
const [imageToolsEnabled, setImageToolsEnabled] = useState(false);
|
const [imageToolsEnabled, setImageToolsEnabled] = useState(false);
|
||||||
|
const [webSearchEnabled, setWebSearchEnabled] = useState(false);
|
||||||
const [contextMode, setContextMode] = useState<'clear' | 'selection' | 'all'>('all');
|
const [contextMode, setContextMode] = useState<'clear' | 'selection' | 'all'>('all');
|
||||||
const [applicationMode, setApplicationMode] = useState<'replace' | 'insert' | 'append'>('append');
|
const [applicationMode, setApplicationMode] = useState<'replace' | 'insert' | 'append'>('append');
|
||||||
const [templates, setTemplates] = useState<Array<{ name: string; template: string }>>([]);
|
const [templates, setTemplates] = useState<Array<{ name: string; template: string }>>([]);
|
||||||
const [streamMode, setStreamMode] = useState(false);
|
const [streamMode, setStreamMode] = useState(false);
|
||||||
|
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||||
|
|
||||||
// Generation state
|
// Generation state
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
@ -48,6 +60,70 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
const lastSelectionRef = useRef<{ isAtStart: boolean } | null>(null);
|
const lastSelectionRef = useRef<{ isAtStart: boolean } | null>(null);
|
||||||
const insertTransactionRef = useRef<{ text: string, onComplete: () => void } | 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) => {
|
const handleContentChange = useCallback((newContent: string) => {
|
||||||
setContent(newContent);
|
setContent(newContent);
|
||||||
|
|
||||||
@ -81,29 +157,7 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addToHistory = useCallback((text: string) => {
|
// History handlers replaced by hook
|
||||||
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]);
|
|
||||||
|
|
||||||
const getProviderApiKey = useCallback(async (provider: string): Promise<string | null> => {
|
const getProviderApiKey = useCallback(async (provider: string): Promise<string | null> => {
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
@ -121,15 +175,9 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: userProvider, error } = await supabase
|
const userProvider = await getProviderConfig(user.id, provider);
|
||||||
.from('provider_configs')
|
|
||||||
.select('settings')
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.eq('name', provider)
|
|
||||||
.eq('is_active', true)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error || !userProvider) {
|
if (!userProvider) {
|
||||||
if (provider !== 'openai') {
|
if (provider !== 'openai') {
|
||||||
// console.warn(\`No provider configuration found for \${provider}\`);
|
// console.warn(\`No provider configuration found for \${provider}\`);
|
||||||
}
|
}
|
||||||
@ -240,7 +288,7 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
abortControllerRef.current = new AbortController();
|
abortControllerRef.current = new AbortController();
|
||||||
const signal = abortControllerRef.current.signal;
|
const signal = abortControllerRef.current.signal;
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
addToHistory(prompt);
|
addPromptToHistory(prompt);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build context prompt
|
// Build context prompt
|
||||||
@ -384,7 +432,7 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
prompt, user, applicationMode, contextMode, selectedText, content, imageToolsEnabled,
|
prompt, user, applicationMode, contextMode, selectedText, content, imageToolsEnabled,
|
||||||
selectedProvider, selectedModel, addToHistory, generateWithProvider, getProviderApiKey, streamMode
|
selectedProvider, selectedModel, addPromptToHistory, generateWithProvider, getProviderApiKey, streamMode
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleOptimize = useCallback(async () => {
|
const handleOptimize = useCallback(async () => {
|
||||||
@ -472,7 +520,7 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [isRecording]);
|
}, [isRecording]);
|
||||||
|
|
||||||
const handleSaveTemplate = useCallback(() => {
|
const handleSaveTemplate = useCallback(async () => {
|
||||||
if (!prompt.trim()) {
|
if (!prompt.trim()) {
|
||||||
toast.error('Please enter a prompt to save');
|
toast.error('Please enter a prompt to save');
|
||||||
return;
|
return;
|
||||||
@ -480,15 +528,43 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
|
|
||||||
const name = window.prompt('Enter template name:');
|
const name = window.prompt('Enter template name:');
|
||||||
if (name) {
|
if (name) {
|
||||||
setTemplates(prev => [...prev, { name, template: prompt }]);
|
const newTemplates = [...templates, { name, template: prompt }];
|
||||||
|
setTemplates(newTemplates);
|
||||||
toast.success('Template saved!');
|
toast.success('Template saved!');
|
||||||
}
|
|
||||||
}, [prompt]);
|
|
||||||
|
|
||||||
const handleDeleteTemplate = useCallback((index: number) => {
|
// Save to server
|
||||||
setTemplates(prev => prev.filter((_, i) => i !== index));
|
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');
|
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) => {
|
const handleApplyTemplate = useCallback((template: string) => {
|
||||||
setPrompt(template);
|
setPrompt(template);
|
||||||
@ -539,6 +615,8 @@ const PlaygroundEditorLLM: React.FC = () => {
|
|||||||
onModelChange={setSelectedModel}
|
onModelChange={setSelectedModel}
|
||||||
imageToolsEnabled={imageToolsEnabled}
|
imageToolsEnabled={imageToolsEnabled}
|
||||||
onImageToolsChange={setImageToolsEnabled}
|
onImageToolsChange={setImageToolsEnabled}
|
||||||
|
webSearchEnabled={webSearchEnabled}
|
||||||
|
onWebSearchChange={setWebSearchEnabled}
|
||||||
contextMode={contextMode}
|
contextMode={contextMode}
|
||||||
onContextModeChange={setContextMode}
|
onContextModeChange={setContextMode}
|
||||||
hasSelection={!!selectedText && selectedText.trim().length > 0}
|
hasSelection={!!selectedText && selectedText.trim().length > 0}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { CompactRenderer } from "./Post/renderers/CompactRenderer";
|
|||||||
import { usePostActions } from "./Post/usePostActions";
|
import { usePostActions } from "./Post/usePostActions";
|
||||||
import { exportMarkdown, downloadMediaItem } from "./Post/PostActions";
|
import { exportMarkdown, downloadMediaItem } from "./Post/PostActions";
|
||||||
import { DeleteDialog } from "./Post/components/DeleteDialogs";
|
import { DeleteDialog } from "./Post/components/DeleteDialogs";
|
||||||
|
|
||||||
import '@vidstack/react/player/styles/default/theme.css';
|
import '@vidstack/react/player/styles/default/theme.css';
|
||||||
import '@vidstack/react/player/styles/default/layouts/video.css';
|
import '@vidstack/react/player/styles/default/layouts/video.css';
|
||||||
|
|
||||||
@ -426,9 +427,17 @@ const Post = ({ postId: propPostId, embedded = false, className }: PostProps) =>
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mediaItem) {
|
if (mediaItem) {
|
||||||
loadVersions();
|
// loadVersions(); // Deprecated: Versions handled by server aggregation
|
||||||
fetchAuthorProfile();
|
// fetchAuthorProfile(); // Deprecated: Author returned in post details
|
||||||
checkIfLiked(mediaItem.id);
|
// 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]);
|
}, [mediaItem, user]);
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,33 @@
|
|||||||
export * from '@/lib/db';
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ArrowLeft, FileText, Calendar, Eye, EyeOff, Edit, Edit3, Check, X, Plus, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
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 { T, translate } from "@/i18n";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@ -19,6 +18,7 @@ import { TableOfContents } from "@/components/sidebar/TableOfContents";
|
|||||||
import { MobileTOC } from "@/components/sidebar/MobileTOC";
|
import { MobileTOC } from "@/components/sidebar/MobileTOC";
|
||||||
import { extractHeadings, extractHeadingsFromLayout, MarkdownHeading } from "@/lib/toc";
|
import { extractHeadings, extractHeadingsFromLayout, MarkdownHeading } from "@/lib/toc";
|
||||||
import { useLayout } from "@/contexts/LayoutContext";
|
import { useLayout } from "@/contexts/LayoutContext";
|
||||||
|
import { fetchUserPage, invalidateUserPageCache } from "@/lib/db";
|
||||||
|
|
||||||
interface Page {
|
interface Page {
|
||||||
id: string;
|
id: string;
|
||||||
@ -54,12 +54,15 @@ interface UserPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initialPage }: 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 navigate = useNavigate();
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser } = useAuth();
|
||||||
const { getLoadedPageLayout, loadPageLayout } = useLayout();
|
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 slug = propSlug || paramSlug;
|
||||||
|
|
||||||
const [page, setPage] = useState<Page | null>(initialPage || null);
|
const [page, setPage] = useState<Page | null>(initialPage || null);
|
||||||
@ -90,94 +93,45 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (userId && slug) {
|
|
||||||
fetchPage();
|
// Determine effective identifier
|
||||||
fetchUserProfile();
|
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) => {
|
||||||
try {
|
|
||||||
let query = supabase
|
|
||||||
.from('pages')
|
|
||||||
.select('id, title, slug, visible, is_public')
|
|
||||||
.eq('parent', parentId)
|
|
||||||
.order('title');
|
|
||||||
|
|
||||||
if (!isOwner) {
|
|
||||||
query = query.eq('visible', true).eq('is_public', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await fetchUserPage(id, slugStr);
|
||||||
|
|
||||||
let query = supabase
|
if (data) {
|
||||||
.from('pages')
|
setPage(data.page);
|
||||||
.select('*')
|
setUserProfile(data.userProfile as any); // Cast to match local interface if needed
|
||||||
.eq('slug', slug)
|
setChildPages(data.childPages || []);
|
||||||
.eq('owner', userId);
|
|
||||||
|
|
||||||
// If not owner, only show public and visible pages
|
// If we resolved via username, ensure we have the userId for isOwner check
|
||||||
if (!isOwner) {
|
if (!resolvedUserId && data.page.owner) {
|
||||||
query = query.eq('is_public', true).eq('visible', true);
|
setResolvedUserId(data.page.owner);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const { data, error } = await query.maybeSingle();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
toast.error(translate('Page not found or you do not have access'));
|
toast.error(translate('Page not found or you do not have access'));
|
||||||
navigate(orgSlug ? `/org/${orgSlug}/user/${userId}` : `/user/${userId}`);
|
navigate(orgSlug ? `/org/${orgSlug}/user/${id}` : `/user/${id}`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPage(data as Page);
|
|
||||||
|
|
||||||
// Fetch parent page if it exists
|
|
||||||
if (data.parent) {
|
|
||||||
fetchParentPage(data.parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch child pages
|
|
||||||
fetchChildPages(data.id);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching page:', error);
|
console.error('Error fetching user page:', error);
|
||||||
toast.error(translate('Failed to load page'));
|
toast.error(translate('Failed to load page'));
|
||||||
navigate(orgSlug ? `/org/${orgSlug}/user/${userId}` : `/user/${userId}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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
|
// Reactive Heading Extraction
|
||||||
// This ensures we extract headings whenever the page loads OR when specific layouts are loaded into context
|
// 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
|
}, [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
|
// Actions now handled by PageActions component
|
||||||
const handlePageUpdate = (updatedPage: Page) => {
|
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 !== page?.parent) {
|
||||||
if (!updatedPage.parent) {
|
// Re-fetch everything to get correct parent details
|
||||||
// Parent removed
|
if (userId && updatedPage.slug) {
|
||||||
setPage({ ...updatedPage, parent_page: null });
|
fetchUserPageData(userId, updatedPage.slug);
|
||||||
} else {
|
|
||||||
// Parent changed, fetch new details
|
|
||||||
setPage(updatedPage);
|
|
||||||
fetchParentPage(updatedPage.parent);
|
|
||||||
}
|
}
|
||||||
// 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 {
|
} else {
|
||||||
setPage(updatedPage);
|
setPage(updatedPage);
|
||||||
}
|
}
|
||||||
@ -305,6 +236,8 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
|
|||||||
|
|
||||||
setPage({ ...page, title: titleValue.trim() });
|
setPage({ ...page, title: titleValue.trim() });
|
||||||
setEditingTitle(false);
|
setEditingTitle(false);
|
||||||
|
// Invalidate cache for this page
|
||||||
|
if (userId && page.slug) invalidateUserPageCache(userId, page.slug);
|
||||||
toast.success(translate('Title updated'));
|
toast.success(translate('Title updated'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating title:', 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);
|
console.error('Error updating slug:', error);
|
||||||
toast.error(translate('Failed to update slug'));
|
toast.error(translate('Failed to update slug'));
|
||||||
} finally {
|
} 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);
|
setSavingField(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -390,6 +325,7 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
|
|||||||
console.error('Error updating tags:', error);
|
console.error('Error updating tags:', error);
|
||||||
toast.error(translate('Failed to update tags'));
|
toast.error(translate('Failed to update tags'));
|
||||||
} finally {
|
} finally {
|
||||||
|
if (userId && page?.slug) invalidateUserPageCache(userId, page.slug);
|
||||||
setSavingField(null);
|
setSavingField(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -709,6 +645,7 @@ const UserPage = ({ userId: propUserId, slug: propSlug, embedded = false, initia
|
|||||||
pageName={page.title}
|
pageName={page.title}
|
||||||
isEditMode={isEditMode && isOwner}
|
isEditMode={isEditMode && isOwner}
|
||||||
showControls={isEditMode && isOwner}
|
showControls={isEditMode && isOwner}
|
||||||
|
initialLayout={page.content}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import { ThemeToggle } from "@/components/ThemeToggle";
|
|||||||
import UserPictures from "@/components/UserPictures";
|
import UserPictures from "@/components/UserPictures";
|
||||||
import { T, translate } from "@/i18n";
|
import { T, translate } from "@/i18n";
|
||||||
import { normalizeMediaType } from "@/lib/mediaRegistry";
|
import { normalizeMediaType } from "@/lib/mediaRegistry";
|
||||||
|
import { useFeedData } from "@/hooks/useFeedData";
|
||||||
|
import * as db from "@/lib/db";
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
@ -42,6 +44,7 @@ const UserProfile = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser } = useAuth();
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
console.log(userProfile, "userProfile");
|
||||||
|
|
||||||
// Post states
|
// Post states
|
||||||
const [publicPosts, setPublicPosts] = useState<any[]>([]);
|
const [publicPosts, setPublicPosts] = useState<any[]>([]);
|
||||||
@ -49,25 +52,69 @@ const UserProfile = () => {
|
|||||||
|
|
||||||
const [collections, setCollections] = useState<Collection[]>([]);
|
const [collections, setCollections] = useState<Collection[]>([]);
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [activeTab, setActiveTab] = useState<'posts' | 'hidden' | 'pictures'>('posts');
|
const [activeTab, setActiveTab] = useState<'posts' | 'hidden' | 'pictures'>('posts');
|
||||||
|
|
||||||
const isOwnProfile = currentUser?.id === userId;
|
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(() => {
|
useEffect(() => {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
fetchUserPosts();
|
|
||||||
fetchUserCollections();
|
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 () => {
|
const fetchUserProfile = async () => {
|
||||||
|
console.log('fetchUserProfile', userId);
|
||||||
try {
|
try {
|
||||||
const { data: profile, error: profileError } = await supabase
|
const { data: profile, error: profileError } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('*')
|
.select(`
|
||||||
|
*,
|
||||||
|
user_roles (role),
|
||||||
|
user_organizations (
|
||||||
|
role,
|
||||||
|
organizations (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
slug
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`)
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
@ -75,25 +122,44 @@ const UserProfile = () => {
|
|||||||
throw profileError;
|
throw profileError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile) {
|
const newProfile = profile ? {
|
||||||
setUserProfile({
|
|
||||||
id: userId!,
|
|
||||||
username: null,
|
|
||||||
display_name: `User ${userId!.slice(0, 8)}`,
|
|
||||||
bio: null,
|
|
||||||
avatar_url: null,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setUserProfile({
|
|
||||||
id: profile.user_id,
|
id: profile.user_id,
|
||||||
username: profile.username,
|
username: profile.username,
|
||||||
display_name: profile.display_name || `User ${userId!.slice(0, 8)}`,
|
display_name: profile.display_name || `User ${userId!.slice(0, 8)}`,
|
||||||
bio: profile.bio,
|
bio: profile.bio,
|
||||||
avatar_url: profile.avatar_url,
|
avatar_url: profile.avatar_url,
|
||||||
created_at: profile.created_at,
|
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 {
|
||||||
|
setOrganizations([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching user profile:', error);
|
console.error('Error fetching user profile:', error);
|
||||||
toast.error(translate('Failed to load user profile'));
|
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 () => {
|
const fetchUserCollections = async () => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
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) {
|
if (!userProfile) {
|
||||||
return (
|
return (
|
||||||
@ -299,7 +292,7 @@ const UserProfile = () => {
|
|||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="flex justify-center md:justify-start gap-8 mb-6">
|
<div className="flex justify-center md:justify-start gap-8 mb-6">
|
||||||
<div className="text-center">
|
<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 className="text-sm text-muted-foreground"><T>posts</T></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -333,7 +326,7 @@ const UserProfile = () => {
|
|||||||
<div className="flex gap-4 overflow-x-auto pb-2">
|
<div className="flex gap-4 overflow-x-auto pb-2">
|
||||||
{/* View All Collections Link */}
|
{/* View All Collections Link */}
|
||||||
<Link
|
<Link
|
||||||
to={`/user/${userId}/collections`}
|
to={`/user/${userProfile.username || userId}/collections`}
|
||||||
className="flex flex-col items-center gap-2 min-w-0 group"
|
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">
|
<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) => (
|
{collections.slice(0, 3).map((collection) => (
|
||||||
<Link
|
<Link
|
||||||
key={collection.id}
|
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"
|
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">
|
<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 */}
|
{/* Pages Section */}
|
||||||
<div className="border-t p-2">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
@ -472,12 +465,12 @@ const UserProfile = () => {
|
|||||||
<div>
|
<div>
|
||||||
<PhotoGrid
|
<PhotoGrid
|
||||||
customPictures={activeTab === 'posts' ? publicPosts : hiddenPosts}
|
customPictures={activeTab === 'posts' ? publicPosts : hiddenPosts}
|
||||||
customLoading={loading}
|
customLoading={feedLoading}
|
||||||
isOwner={isOwnProfile}
|
isOwner={isOwnProfile}
|
||||||
navigationSource="user"
|
navigationSource="user"
|
||||||
navigationSourceId={userId}
|
navigationSourceId={userId}
|
||||||
/>
|
/>
|
||||||
{isOwnProfile && activeTab === 'posts' && publicPosts.length === 0 && !loading && (
|
{isOwnProfile && activeTab === 'posts' && publicPosts.length === 0 && !feedLoading && (
|
||||||
<div className="text-center mt-8">
|
<div className="text-center mt-8">
|
||||||
<Button
|
<Button
|
||||||
className="bg-primary hover:bg-primary/90 text-white"
|
className="bg-primary hover:bg-primary/90 text-white"
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export interface NavigationPost {
|
|||||||
export interface NavigationData {
|
export interface NavigationData {
|
||||||
posts: NavigationPost[];
|
posts: NavigationPost[];
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid';
|
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid' | 'widget';
|
||||||
sourceId?: string;
|
sourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
import { clientsClaim } from 'workbox-core'
|
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}`);
|
console.log(`[SW] Initializing Version: ${SW_VERSION}`);
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => { });
|
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
|
declare let self: ServiceWorkerGlobalScope
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
self.addEventListener('message', (event) => {
|
||||||
@ -29,7 +30,7 @@ clientsClaim()
|
|||||||
|
|
||||||
// allow only fallback in dev: we don't want to cache everything
|
// allow only fallback in dev: we don't want to cache everything
|
||||||
let allowlist: undefined | RegExp[]
|
let allowlist: undefined | RegExp[]
|
||||||
if (import.meta.env.DEV)
|
if (location.hostname === 'localhost' || location.hostname.includes('127.0.0.1'))
|
||||||
allowlist = [/^\/$/]
|
allowlist = [/^\/$/]
|
||||||
|
|
||||||
// Handle Share Target POST requests
|
// Handle Share Target POST requests
|
||||||
@ -63,7 +64,6 @@ registerRoute(
|
|||||||
'POST'
|
'POST'
|
||||||
);
|
);
|
||||||
|
|
||||||
import { NetworkFirst } from 'workbox-strategies';
|
|
||||||
|
|
||||||
// Navigation handler: Prefer network to get server injection, fallback to index.html
|
// Navigation handler: Prefer network to get server injection, fallback to index.html
|
||||||
const navigationHandler = async (params: any) => {
|
const navigationHandler = async (params: any) => {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export interface MediaItem {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
likes_count: number | null;
|
likes_count: number | null;
|
||||||
|
is_liked?: boolean;
|
||||||
is_selected: boolean;
|
is_selected: boolean;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
tags: string[] | null;
|
tags: string[] | null;
|
||||||
@ -31,7 +32,13 @@ export interface MediaItem {
|
|||||||
parent_id: string | null;
|
parent_id: string | null;
|
||||||
position: number;
|
position: number;
|
||||||
job?: any;
|
job?: any;
|
||||||
|
picture_id?: string;
|
||||||
responsive?: any; // To hold server-generated responsive data
|
responsive?: any; // To hold server-generated responsive data
|
||||||
|
author?: {
|
||||||
|
username: string;
|
||||||
|
display_name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mux Video Resolution Types
|
// Mux Video Resolution Types
|
||||||
@ -174,6 +181,6 @@ export interface PostNavigationData {
|
|||||||
comments_count?: number;
|
comments_count?: number;
|
||||||
}[];
|
}[];
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid';
|
source: 'home' | 'collection' | 'tag' | 'search' | 'user' | 'photogrid' | 'widget';
|
||||||
sourceId?: string;
|
sourceId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user