profile
This commit is contained in:
parent
1fac563618
commit
33adb738f3
@ -30,7 +30,7 @@ import AuthZ from "./pages/AuthZ";
|
||||
|
||||
const UpdatePassword = React.lazy(() => import("./pages/UpdatePassword"));
|
||||
|
||||
import Profile from "./pages/Profile";
|
||||
import Profile from "./modules/profile/Profile";
|
||||
const Post = React.lazy(() => import("./modules/posts/PostPage"));
|
||||
|
||||
import UserProfile from "./pages/UserProfile";
|
||||
@ -279,32 +279,32 @@ const App = () => {
|
||||
<SWRConfig value={{ provider: () => new Map() }}>
|
||||
<OidcProvider {...oidcConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<LogProvider>
|
||||
<MediaRefreshProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<ActionProvider>
|
||||
<BrowserRouter>
|
||||
<DragDropProvider>
|
||||
<ProfilesProvider>
|
||||
<WebSocketProvider url={import.meta.env.VITE_SERVER_IMAGE_API_URL}>
|
||||
<StreamProvider url={import.meta.env.VITE_SERVER_IMAGE_API_URL}>
|
||||
<StreamInvalidator />
|
||||
<FeedCacheProvider>
|
||||
<AppWrapper />
|
||||
</FeedCacheProvider>
|
||||
</StreamProvider>
|
||||
</WebSocketProvider>
|
||||
</ProfilesProvider>
|
||||
</DragDropProvider>
|
||||
</BrowserRouter>
|
||||
</ActionProvider>
|
||||
</TooltipProvider>
|
||||
</MediaRefreshProvider>
|
||||
</LogProvider>
|
||||
</AuthProvider>
|
||||
<AuthProvider>
|
||||
<LogProvider>
|
||||
<MediaRefreshProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<ActionProvider>
|
||||
<BrowserRouter>
|
||||
<DragDropProvider>
|
||||
<ProfilesProvider>
|
||||
<WebSocketProvider url={import.meta.env.VITE_SERVER_IMAGE_API_URL}>
|
||||
<StreamProvider url={import.meta.env.VITE_SERVER_IMAGE_API_URL}>
|
||||
<StreamInvalidator />
|
||||
<FeedCacheProvider>
|
||||
<AppWrapper />
|
||||
</FeedCacheProvider>
|
||||
</StreamProvider>
|
||||
</WebSocketProvider>
|
||||
</ProfilesProvider>
|
||||
</DragDropProvider>
|
||||
</BrowserRouter>
|
||||
</ActionProvider>
|
||||
</TooltipProvider>
|
||||
</MediaRefreshProvider>
|
||||
</LogProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</OidcProvider>
|
||||
</SWRConfig>
|
||||
|
||||
@ -39,6 +39,10 @@ interface MediaCardProps {
|
||||
apiUrl?: string;
|
||||
versionCount?: number;
|
||||
preset?: CardPreset;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
showAuthor?: boolean;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
const MediaCard: React.FC<MediaCardProps> = ({
|
||||
@ -67,7 +71,11 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
||||
variant = 'grid',
|
||||
apiUrl,
|
||||
versionCount,
|
||||
preset
|
||||
preset,
|
||||
showTitle,
|
||||
showDescription,
|
||||
showAuthor,
|
||||
showActions
|
||||
}) => {
|
||||
const normalizedType = normalizeMediaType(type);
|
||||
// Render based on type
|
||||
@ -105,6 +113,10 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
||||
job={job}
|
||||
variant={variant}
|
||||
apiUrl={apiUrl}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
showAuthor={showAuthor}
|
||||
showActions={showActions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -194,6 +206,10 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
||||
variant={variant}
|
||||
apiUrl={apiUrl}
|
||||
preset={preset}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
showAuthor={showAuthor}
|
||||
showActions={showActions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -222,6 +238,10 @@ const MediaCard: React.FC<MediaCardProps> = ({
|
||||
apiUrl={apiUrl}
|
||||
versionCount={versionCount}
|
||||
preset={preset}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
showAuthor={showAuthor}
|
||||
showActions={showActions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -49,6 +49,10 @@ interface VideoCardProps {
|
||||
variant?: 'grid' | 'feed';
|
||||
showPlayButton?: boolean;
|
||||
apiUrl?: string;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
showAuthor?: boolean;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
const VideoCard = ({
|
||||
@ -73,7 +77,11 @@ const VideoCard = ({
|
||||
created_at,
|
||||
job,
|
||||
variant = 'grid',
|
||||
apiUrl
|
||||
apiUrl,
|
||||
showTitle = true,
|
||||
showDescription = true,
|
||||
showAuthor = true,
|
||||
showActions = true
|
||||
}: VideoCardProps) => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
@ -574,15 +582,17 @@ const VideoCard = ({
|
||||
<div className={`${variant === 'grid' ? "md:hidden" : ""} pb-2 space-y-2`}>
|
||||
{/* Row 1: Avatar + Actions */}
|
||||
<div className="flex items-center justify-between px-2 pt-2">
|
||||
<UserAvatarBlock
|
||||
userId={authorId}
|
||||
avatarUrl={authorAvatarUrl}
|
||||
displayName={author === 'User' ? undefined : author}
|
||||
className="w-8 h-8"
|
||||
showDate={false}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{showAuthor && (
|
||||
<UserAvatarBlock
|
||||
userId={authorId}
|
||||
avatarUrl={authorAvatarUrl}
|
||||
displayName={author === 'User' ? undefined : author}
|
||||
className="w-8 h-8"
|
||||
showDate={false}
|
||||
/>
|
||||
)}
|
||||
{showActions && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
@ -634,6 +644,7 @@ const VideoCard = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Likes */}
|
||||
@ -641,11 +652,11 @@ const VideoCard = ({
|
||||
|
||||
{/* Caption / Description section */}
|
||||
<div className="px-4 space-y-1">
|
||||
{(!isLikelyFilename(title) && title) && (
|
||||
{showTitle && (!isLikelyFilename(title) && title) && (
|
||||
<div className="font-semibold text-sm">{title}</div>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
{showDescription && description && (
|
||||
<div className="text-sm text-foreground/90 line-clamp-3 pl-8">
|
||||
<MarkdownRenderer content={description} className="prose-sm dark:prose-invert" />
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FeedPost } from '@/modules/posts/client-posts';
|
||||
import { FeedCarousel } from './FeedCarousel';
|
||||
import { Heart } from 'lucide-react';
|
||||
import { Heart, MessageCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as db from '@/lib/db';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { normalizeMediaType } from "@/lib/mediaRegistry";
|
||||
import { toggleLike } from '@/modules/posts/client-pictures';
|
||||
import UserAvatarBlock from '@/components/UserAvatarBlock';
|
||||
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||
import { formatDate } from '@/utils/textUtils';
|
||||
import { T } from '@/i18n';
|
||||
|
||||
interface FeedCardProps {
|
||||
post: FeedPost;
|
||||
@ -15,13 +19,17 @@ interface FeedCardProps {
|
||||
onComment?: () => void;
|
||||
onShare?: () => void;
|
||||
onNavigate?: (id: string) => void;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
}
|
||||
|
||||
export const FeedCard: React.FC<FeedCardProps> = ({
|
||||
post,
|
||||
currentUserId,
|
||||
onLike,
|
||||
onNavigate
|
||||
onNavigate,
|
||||
showTitle = false,
|
||||
showDescription = false,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
// Initialize from precomputed status (post.cover.is_liked or post.is_liked)
|
||||
@ -104,7 +112,18 @@ export const FeedCard: React.FC<FeedCardProps> = ({
|
||||
|
||||
return (
|
||||
<article className="bg-background md:border md:mb-6 md:pb-0 overflow-hidden">
|
||||
|
||||
{/* Header: Author Info */}
|
||||
<div className="px-3 py-2 flex items-center justify-between">
|
||||
<UserAvatarBlock
|
||||
userId={post.user_id}
|
||||
avatarUrl={post.author?.avatar_url}
|
||||
displayName={post.author?.display_name || post.author?.username}
|
||||
showDate={false}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{post.created_at ? formatDate(post.created_at) : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Media Carousel */}
|
||||
<div className="relative" onTouchEnd={handleDoubleTap} onClick={handleDoubleTap}>
|
||||
@ -116,18 +135,52 @@ export const FeedCard: React.FC<FeedCardProps> = ({
|
||||
authorId={post.user_id}
|
||||
authorAvatarUrl={post.author?.avatar_url}
|
||||
onItemClick={handleItemClick}
|
||||
showContent={false} // Silence internal card UI
|
||||
showAuthor={false}
|
||||
showActions={false}
|
||||
showTitle={false}
|
||||
showDescription={false}
|
||||
/>
|
||||
|
||||
{/* Double tap heart animation overlay */}
|
||||
<div className={cn(
|
||||
"absolute inset-0 flex items-center justify-center pointer-events-none transition-opacity duration-300",
|
||||
showHeartAnimation ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||
)}>
|
||||
<Heart className="w-24 h-24 text-white fill-white drop-shadow-xl animate-bounce-short" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Bar - Removed as actions are now per-item in the carousel */}
|
||||
{/* Actions Bar */}
|
||||
<div className="flex items-center px-2 pt-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleLike}
|
||||
className={cn("hover:text-red-500", isLiked && "text-red-500")}
|
||||
>
|
||||
<Heart className="h-6 w-6" fill={isLiked ? "currentColor" : "none"} />
|
||||
</Button>
|
||||
{likeCount > 0 && (
|
||||
<span className="text-sm font-medium mr-1">{likeCount}</span>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-foreground"
|
||||
onClick={() => {
|
||||
// Pass ID to parent but if it's not implemented, navigate to details
|
||||
onNavigate?.(post.id);
|
||||
}}
|
||||
>
|
||||
<MessageCircle className="h-6 w-6 -rotate-90" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Caption: Title & Description */}
|
||||
<div className="px-4 pb-3 space-y-1">
|
||||
{showTitle && post.title && (
|
||||
<div className="font-bold text-sm">{post.title}</div>
|
||||
)}
|
||||
{showDescription && post.description && (
|
||||
<div className="text-sm text-foreground/90 line-clamp-3">
|
||||
<MarkdownRenderer content={post.description} className="prose-sm dark:prose-invert" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
@ -14,6 +14,10 @@ interface CarouselProps {
|
||||
authorAvatarUrl?: string | null;
|
||||
onItemClick?: (id: string) => void;
|
||||
showContent?: boolean;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
showAuthor?: boolean;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
export const FeedCarousel: React.FC<CarouselProps> = ({
|
||||
@ -25,7 +29,11 @@ export const FeedCarousel: React.FC<CarouselProps> = ({
|
||||
authorId,
|
||||
authorAvatarUrl,
|
||||
onItemClick,
|
||||
showContent = true
|
||||
showContent = true,
|
||||
showTitle,
|
||||
showDescription,
|
||||
showAuthor,
|
||||
showActions
|
||||
}) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false });
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@ -92,6 +100,10 @@ export const FeedCarousel: React.FC<CarouselProps> = ({
|
||||
description={item.description}
|
||||
created_at={item.created_at}
|
||||
showContent={showContent}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
showAuthor={showAuthor}
|
||||
showActions={showActions}
|
||||
onClick={onItemClick}
|
||||
job={item.job}
|
||||
responsive={item.responsive}
|
||||
|
||||
@ -20,10 +20,10 @@ interface MobileFeedProps {
|
||||
contentType?: 'posts' | 'pages' | 'pictures' | 'files';
|
||||
visibilityFilter?: 'invisible' | 'private';
|
||||
center?: boolean;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
}
|
||||
|
||||
const PRELOAD_BUFFER = 3;
|
||||
|
||||
export const MobileFeed: React.FC<MobileFeedProps> = ({
|
||||
source = 'home',
|
||||
sourceId,
|
||||
@ -33,7 +33,9 @@ export const MobileFeed: React.FC<MobileFeedProps> = ({
|
||||
categoryIds,
|
||||
contentType,
|
||||
visibilityFilter,
|
||||
center
|
||||
center,
|
||||
showTitle,
|
||||
showDescription,
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
@ -140,6 +142,8 @@ export const MobileFeed: React.FC<MobileFeedProps> = ({
|
||||
posts={posts}
|
||||
currentUser={user}
|
||||
onNavigate={onNavigate}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
/>
|
||||
));
|
||||
}
|
||||
@ -148,8 +152,8 @@ export const MobileFeed: React.FC<MobileFeedProps> = ({
|
||||
if (item.type === 'page-vfs-folder') return 'Folders';
|
||||
if (item._searchSource === 'picture') return 'Pictures';
|
||||
if (item._searchSource === 'file') {
|
||||
if (item.thumbnail_url || item.cover || (item.pictures && item.pictures.length > 0)) return 'Pictures';
|
||||
return 'Files';
|
||||
if (item.thumbnail_url || item.cover || (item.pictures && item.pictures.length > 0)) return 'Pictures';
|
||||
return 'Files';
|
||||
}
|
||||
if (item._searchSource === 'page') return 'Pages';
|
||||
if (item._searchSource === 'post') return 'Posts';
|
||||
@ -168,13 +172,13 @@ export const MobileFeed: React.FC<MobileFeedProps> = ({
|
||||
|
||||
for (const group of orderedGroups) {
|
||||
if (groups.has(group)) {
|
||||
elements.push(
|
||||
elements.push(
|
||||
<div key={`group-${group}`} className="px-3 py-1.5 bg-muted/40 border-y text-xs font-semibold uppercase tracking-wider text-muted-foreground sticky top-0 z-10 backdrop-blur-md">
|
||||
<T>{group}</T>
|
||||
</div>
|
||||
);
|
||||
elements.push(
|
||||
...groups.get(group)!.map((post: any, index: number) => (
|
||||
);
|
||||
elements.push(
|
||||
...groups.get(group)!.map((post: any, index: number) => (
|
||||
<FeedItemWrapper
|
||||
key={post.id}
|
||||
post={post}
|
||||
@ -182,9 +186,11 @@ export const MobileFeed: React.FC<MobileFeedProps> = ({
|
||||
posts={posts}
|
||||
currentUser={user}
|
||||
onNavigate={onNavigate}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
/>
|
||||
))
|
||||
);
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,38 +210,24 @@ const FeedItemWrapper: React.FC<{
|
||||
index: number,
|
||||
posts: FeedPost[],
|
||||
currentUser: any,
|
||||
onNavigate?: (id: string) => void
|
||||
}> = ({ post, index, posts, currentUser, onNavigate }) => {
|
||||
const { ref, inView } = useInView({
|
||||
onNavigate?: (id: string) => void;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
}> = ({ post, index, posts, currentUser, onNavigate, showTitle, showDescription }) => {
|
||||
const { ref } = useInView({
|
||||
triggerOnce: false,
|
||||
rootMargin: '200px 0px', // Trigger slightly before
|
||||
rootMargin: '200px 0px',
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
if (inView) {
|
||||
// Preload next 5 posts' Main Image
|
||||
const bufferEnd = Math.min(index + 1 + PRELOAD_BUFFER, posts.length);
|
||||
for (let i = index + 1; i < bufferEnd; i++) {
|
||||
const nextPost = posts[i];
|
||||
if (nextPost.cover?.image_url) {
|
||||
const img = new Image();
|
||||
img.src = nextPost.cover.image_url;
|
||||
}
|
||||
// If the next post has multiple images, maybe preload the second one too?
|
||||
// Keeping it light: only cover for now.
|
||||
}
|
||||
}
|
||||
*/
|
||||
}, [inView, index, posts]);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<FeedCard
|
||||
post={post}
|
||||
currentUserId={currentUser?.id}
|
||||
onNavigate={onNavigate}
|
||||
showTitle={showTitle}
|
||||
showDescription={showDescription}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -339,7 +339,7 @@ const HomeWidget: React.FC<HomeWidgetProps> = ({
|
||||
return viewMode === 'list' ? (
|
||||
<ListLayout key={refreshKey} sortBy={sortBy} navigationSource={feedSource} navigationSourceId={feedSourceId} categorySlugs={categorySlugs} categoryIds={propCategoryId ? [propCategoryId] : undefined} contentType={contentType} visibilityFilter={visibilityFilter} center={center} />
|
||||
) : (
|
||||
<MobileFeed source={feedSource} sourceId={feedSourceId} sortBy={sortBy} categorySlugs={categorySlugs} categoryIds={propCategoryId ? [propCategoryId] : undefined} contentType={contentType} visibilityFilter={visibilityFilter} onNavigate={(id) => window.location.href = `/post/${id}`} />
|
||||
<MobileFeed source={feedSource} sourceId={feedSourceId} sortBy={sortBy} categorySlugs={categorySlugs} categoryIds={propCategoryId ? [propCategoryId] : undefined} contentType={contentType} visibilityFilter={visibilityFilter} onNavigate={(id) => window.location.href = `/post/${id}`} showTitle={showTitle} showDescription={showDescription} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,10 @@ interface PageCardProps extends Omit<MediaRendererProps, 'created_at'> {
|
||||
versionCount?: number;
|
||||
preset?: CardPreset;
|
||||
className?: string;
|
||||
showTitle?: boolean;
|
||||
showDescription?: boolean;
|
||||
showAuthor?: boolean;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
const PageCard: React.FC<PageCardProps> = ({
|
||||
@ -52,7 +56,11 @@ const PageCard: React.FC<PageCardProps> = ({
|
||||
versionCount,
|
||||
preset,
|
||||
type,
|
||||
className
|
||||
className,
|
||||
showTitle,
|
||||
showDescription,
|
||||
showAuthor,
|
||||
showActions
|
||||
}) => {
|
||||
// Determine image source
|
||||
// If url is missing or empty, fallback to picsum
|
||||
@ -94,7 +102,7 @@ const PageCard: React.FC<PageCardProps> = ({
|
||||
<div className="pb-2 space-y-2">
|
||||
{/* Author + Actions row */}
|
||||
<div className="flex items-center justify-between px-2 pt-2">
|
||||
{preset?.showAuthor !== false && (
|
||||
{(showAuthor ?? preset?.showAuthor) !== false && (
|
||||
<UserAvatarBlock
|
||||
userId={authorId}
|
||||
avatarUrl={authorAvatarUrl}
|
||||
@ -104,7 +112,7 @@ const PageCard: React.FC<PageCardProps> = ({
|
||||
createdAt={created_at}
|
||||
/>
|
||||
)}
|
||||
{preset?.showActions !== false && (
|
||||
{(showActions ?? preset?.showActions) !== false && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="ghost" className="px-0 gap-1" onClick={handleLike}>
|
||||
<Heart className={`h-5 w-5 ${isLiked ? "fill-red-500 text-red-500" : ""}`} />
|
||||
@ -120,8 +128,10 @@ const PageCard: React.FC<PageCardProps> = ({
|
||||
|
||||
{/* Title & Description */}
|
||||
<div className="px-4 space-y-1">
|
||||
<div className="font-semibold text-sm">{title}</div>
|
||||
{description && (
|
||||
{(showTitle ?? preset?.showTitle) !== false && (
|
||||
<div className="font-semibold text-sm">{title}</div>
|
||||
)}
|
||||
{(showDescription ?? preset?.showDescription) !== false && description && (
|
||||
<div className="text-sm text-foreground/90 line-clamp-3">
|
||||
<MarkdownRenderer content={description} className="prose-sm dark:prose-invert" />
|
||||
</div>
|
||||
|
||||
@ -20,6 +20,8 @@ import { ApiKeysSettings } from "@/modules/profile/components/ApiKeysSettings";
|
||||
import { ProfileGallery } from "@/modules/profile/components/ProfileGallery";
|
||||
|
||||
// Lazy Loaded Modules
|
||||
const FileBrowser = React.lazy(() => import('@/apps/filebrowser/FileBrowser'));
|
||||
|
||||
const LazyPurchasesList = React.lazy(() =>
|
||||
import("@polymech/ecommerce").then(m => ({ default: m.PurchasesList }))
|
||||
);
|
||||
@ -110,14 +112,14 @@ const Profile = () => {
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="min-h-screen flex w-full bg-background pt-14">
|
||||
<div className="min-h-screen flex w-full bg-background">
|
||||
<ProfileSidebar activeSection={activeSection} onSectionChange={setActiveSection} />
|
||||
|
||||
<main className="flex-1 p-4 overflow-auto">
|
||||
<main className="flex-1 p-2 overflow-auto">
|
||||
<div className="mx-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>General Settings</T></CardTitle>
|
||||
</CardHeader>
|
||||
@ -140,11 +142,11 @@ const Profile = () => {
|
||||
onSelectFromGallery: handleSelectFromGallery
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="api-keys" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>API Keys</T></CardTitle>
|
||||
</CardHeader>
|
||||
@ -155,11 +157,11 @@ const Profile = () => {
|
||||
onSubmit={handleProfileUpdate}
|
||||
updating={updatingProfile}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="variables" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>Variables</T></CardTitle>
|
||||
</CardHeader>
|
||||
@ -175,33 +177,33 @@ const Profile = () => {
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="addresses" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>Shipping Addresses</T></CardTitle>
|
||||
</CardHeader>
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<ShippingAddressManager userId={user.id} />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="vendor" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>Vendor Profiles</T></CardTitle>
|
||||
</CardHeader>
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<VendorProfileManager userId={user.id} />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="purchases" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>My Purchases</T></CardTitle>
|
||||
</CardHeader>
|
||||
@ -219,68 +221,68 @@ const Profile = () => {
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="contacts" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>Contacts</T></CardTitle>
|
||||
</CardHeader>
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<ContactsManager />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="campaigns" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>Campaigns</T></CardTitle>
|
||||
</CardHeader>
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<CampaignsManager />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="categories" element={
|
||||
<Card className="flex flex-col h-[75vh] min-h-[600px]">
|
||||
<div className="flex flex-col h-[75vh] min-h-[600px]">
|
||||
<CardHeader>
|
||||
<CardTitle><T>Categories</T></CardTitle>
|
||||
</CardHeader>
|
||||
<div className="flex-1 min-h-0 pl-6 pr-6 pb-6">
|
||||
<div className="flex-1 min-h-0 px-0 pb-1 md:px-6 md:pb-6 relative overflow-hidden">
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<CategoryManager isOpen={true} onClose={() => { }} asView={true} />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="integrations" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>IMAP Integrations</T></CardTitle>
|
||||
</CardHeader>
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<GmailIntegrations />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="smtp-servers" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>SMTP Servers</T></CardTitle>
|
||||
</CardHeader>
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<SmtpIntegrations />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="gallery" element={
|
||||
<Card>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle><T>My Gallery</T></CardTitle>
|
||||
</CardHeader>
|
||||
@ -292,7 +294,25 @@ const Profile = () => {
|
||||
onDelete={handleImageDelete}
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
} />
|
||||
|
||||
<Route path="my-files/*" element={
|
||||
<div className="flex flex-col h-[calc(100vh-140px)] min-h-[500px] overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle><T>My Files</T></CardTitle>
|
||||
</CardHeader>
|
||||
<div className="flex-1 min-h-0 px-2 pb-1 md:px-6 md:pb-6 relative overflow-hidden">
|
||||
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
||||
<FileBrowser
|
||||
allowPanels={false}
|
||||
mode="simple"
|
||||
disableRoutingSync={true}
|
||||
initialMount="home"
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</div>
|
||||
} />
|
||||
</Routes>
|
||||
</div>
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { Images } from "lucide-react";
|
||||
import { T } from "@/i18n";
|
||||
import {
|
||||
Sidebar,
|
||||
@ -25,10 +24,7 @@ export const ProfileSidebar: React.FC<ProfileSidebarProps> = ({
|
||||
}) => {
|
||||
const { state } = useSidebar();
|
||||
const isCollapsed = state === "collapsed";
|
||||
|
||||
const mainItems = PROFILE_MENU_ITEMS.filter(item => item.id !== 'gallery');
|
||||
const galleryItem = PROFILE_MENU_ITEMS.find(item => item.id === 'gallery');
|
||||
|
||||
const mainItems = PROFILE_MENU_ITEMS;
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarContent>
|
||||
@ -50,24 +46,6 @@ export const ProfileSidebar: React.FC<ProfileSidebarProps> = ({
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup className="mt-auto">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{galleryItem && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => onSectionChange('gallery')}
|
||||
className={activeSection === 'gallery' ? "bg-muted text-primary font-medium" : "hover:bg-muted/50"}
|
||||
>
|
||||
<galleryItem.icon className="h-4 w-4" />
|
||||
{!isCollapsed && <span><T>{galleryItem.label}</T></span>}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { User, Key, Hash, MapPin, Building2, BookUser, Send, Plug, ShoppingBag, Images, FolderTree } from "lucide-react";
|
||||
import { User, Key, Hash, MapPin, Building2, BookUser, Send, Plug, ShoppingBag, Images, FolderTree, FolderOpen } from "lucide-react";
|
||||
import { translate } from "@/i18n";
|
||||
|
||||
export type ActiveSection = 'general' | 'api-keys' | 'variables' | 'addresses' | 'vendor' | 'gallery' | 'purchases' | 'contacts' | 'campaigns' | 'integrations' | 'smtp-servers' | 'categories';
|
||||
export type ActiveSection = 'general' | 'api-keys' | 'variables' | 'addresses' | 'vendor' | 'gallery' | 'purchases' | 'contacts' | 'campaigns' | 'integrations' | 'smtp-servers' | 'categories' | 'my-files';
|
||||
|
||||
export interface ProfileData {
|
||||
username: string;
|
||||
@ -14,7 +14,7 @@ export interface ProfileData {
|
||||
export const PROFILE_MENU_ITEMS = [
|
||||
{ id: 'general' as ActiveSection, label: 'General', icon: User },
|
||||
{ id: 'api-keys' as ActiveSection, label: 'API Keys', icon: Key },
|
||||
{ id: 'variables' as ActiveSection, label: 'Hash Variables', icon: Hash },
|
||||
{ id: 'variables' as ActiveSection, label: 'Variables', icon: Hash },
|
||||
{ id: 'categories' as ActiveSection, label: 'Categories', icon: FolderTree },
|
||||
{ id: 'addresses' as ActiveSection, label: 'Shipping Addresses', icon: MapPin },
|
||||
{ id: 'vendor' as ActiveSection, label: 'Vendor Profiles', icon: Building2 },
|
||||
@ -23,5 +23,5 @@ export const PROFILE_MENU_ITEMS = [
|
||||
{ id: 'integrations' as ActiveSection, label: 'IMAP Integrations', icon: Plug },
|
||||
{ id: 'smtp-servers' as ActiveSection, label: 'SMTP Servers', icon: Send },
|
||||
{ id: 'purchases' as ActiveSection, label: 'Purchases', icon: ShoppingBag },
|
||||
{ id: 'gallery' as ActiveSection, label: 'Gallery', icon: Images },
|
||||
{ id: 'my-files' as ActiveSection, label: 'My Files', icon: FolderOpen }
|
||||
];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user