558 lines
32 KiB
TypeScript
558 lines
32 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
export interface HomeWidgetProps {
|
|
sortBy?: 'latest' | 'top';
|
|
viewMode?: 'grid' | 'large' | 'list';
|
|
showCategories?: boolean;
|
|
categorySlugs?: string;
|
|
categoryId?: string;
|
|
userId?: string;
|
|
showSortBar?: boolean;
|
|
showLayoutToggles?: boolean;
|
|
showFooter?: boolean;
|
|
center?: boolean;
|
|
columns?: number | 'auto';
|
|
showTitle?: boolean;
|
|
showDescription?: boolean;
|
|
heading?: string;
|
|
headingLevel?: 'h1' | 'h2' | 'h3' | 'h4';
|
|
variables?: Record<string, any>;
|
|
searchQuery?: string;
|
|
initialContentType?: 'posts' | 'pages' | 'pictures' | 'files';
|
|
initialVisibilityFilter?: 'invisible' | 'private';
|
|
}
|
|
import type { FeedSortOption } from '@/hooks/useFeedData';
|
|
import { useMediaRefresh } from '@/contexts/MediaRefreshContext';
|
|
import { useIsMobile } from '@/hooks/use-mobile';
|
|
import MediaCard from '@/components/MediaCard';
|
|
import { normalizeMediaType } from '@/lib/mediaRegistry';
|
|
|
|
import PhotoGrid from '@/components/PhotoGrid';
|
|
import GalleryLarge from '@/components/GalleryLarge';
|
|
import MobileFeed from '@/components/feed/MobileFeed';
|
|
import { ListLayout } from '@/components/ListLayout';
|
|
import CategoryTreeView from '@/components/CategoryTreeView';
|
|
import Footer from '@/components/Footer';
|
|
import { T } from '@/i18n';
|
|
import { SEO } from '@/components/SEO';
|
|
|
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
|
|
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
|
|
import { LayoutGrid, GalleryVerticalEnd, TrendingUp, Clock, List, FolderTree, FileText, Image as ImageIcon, EyeOff, Lock, SlidersHorizontal, Layers, Camera } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const SIDEBAR_KEY = 'categorySidebarSize';
|
|
const DEFAULT_SIDEBAR = 15;
|
|
|
|
const HomeWidget: React.FC<HomeWidgetProps> = ({
|
|
sortBy: propSortBy = 'latest',
|
|
viewMode: propViewMode = 'grid',
|
|
showCategories: propShowCategories = false,
|
|
categorySlugs: propCategorySlugs = '',
|
|
categoryId: propCategoryId,
|
|
userId: propUserId = '',
|
|
showSortBar = true,
|
|
showLayoutToggles = true,
|
|
showFooter = true,
|
|
showTitle = true,
|
|
showDescription = false,
|
|
center = false,
|
|
columns = 'auto',
|
|
heading,
|
|
headingLevel = 'h2',
|
|
searchQuery,
|
|
initialContentType,
|
|
initialVisibilityFilter,
|
|
}) => {
|
|
const { refreshKey } = useMediaRefresh();
|
|
const isMobile = useIsMobile();
|
|
const { user: currentUser } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
// Derive content type from URL path (/user/:id/... or top-level /posts, /pages)
|
|
const urlContentType = useMemo(() => {
|
|
// Match /user/:id/posts|pages|pictures|files
|
|
const userMatch = location.pathname.match(/^\/user\/[^/]+\/(posts|pages|pictures|files)$/);
|
|
if (userMatch) return userMatch[1] as 'posts' | 'pages' | 'pictures' | 'files';
|
|
// Match top-level /posts or /pages or /files
|
|
const topMatch = location.pathname.match(/^\/(posts|pages|files)$/);
|
|
if (topMatch) return topMatch[1] as 'posts' | 'pages' | 'files';
|
|
return undefined;
|
|
}, [location.pathname]);
|
|
|
|
// Effective initial: prop takes priority, then URL
|
|
const effectiveInitial = initialContentType ?? urlContentType;
|
|
|
|
// Show visibility toggles on own profile OR in search mode when authenticated
|
|
const isOwnProfile = !!(propUserId && currentUser?.id === propUserId) || !!(searchQuery && currentUser);
|
|
|
|
// Local state driven from props (with user overrides)
|
|
const [sortBy, setSortBy] = useState<FeedSortOption>(propSortBy);
|
|
const [viewMode, setViewMode] = useState<'grid' | 'large' | 'list'>(() => {
|
|
return (localStorage.getItem('feedViewMode') as 'grid' | 'large' | 'list') || propViewMode;
|
|
});
|
|
const [showCategories, setShowCategories] = useState(propShowCategories);
|
|
|
|
// Sync from prop changes
|
|
useEffect(() => { setSortBy(propSortBy); }, [propSortBy]);
|
|
useEffect(() => { setShowCategories(propShowCategories); }, [propShowCategories]);
|
|
useEffect(() => { setViewMode(propViewMode); }, [propViewMode]);
|
|
|
|
// Content type filter
|
|
const [contentType, setContentType] = useState<'posts' | 'pages' | 'pictures' | 'files' | undefined>(
|
|
String(effectiveInitial) === 'all' ? undefined : (effectiveInitial as any)
|
|
);
|
|
|
|
// Sync when URL changes (e.g. browser back/forward)
|
|
useEffect(() => {
|
|
setContentType(String(effectiveInitial) === 'all' ? undefined : (effectiveInitial as any));
|
|
}, [effectiveInitial]);
|
|
|
|
// Navigate URL when content type changes (only when rendered on a user profile or in search)
|
|
const handleContentTypeChange = useCallback((newType: 'posts' | 'pages' | 'pictures' | 'files' | undefined) => {
|
|
setContentType(newType);
|
|
if (propUserId) {
|
|
const basePath = `/user/${propUserId}`;
|
|
const targetPath = newType ? `${basePath}/${newType}` : basePath;
|
|
navigate(targetPath, { replace: true });
|
|
} else if (searchQuery) {
|
|
const params = new URLSearchParams(location.search);
|
|
if (newType) params.set('type', newType);
|
|
else params.delete('type');
|
|
navigate(`/search?${params.toString()}`, { replace: true });
|
|
}
|
|
}, [propUserId, searchQuery, location.search, navigate]);
|
|
|
|
// Visibility filter (own profile only) — exclusive: shows ONLY invisible or ONLY private
|
|
const [visibilityFilter, setVisibilityFilter] = useState<'invisible' | 'private' | undefined>(initialVisibilityFilter);
|
|
|
|
useEffect(() => {
|
|
setVisibilityFilter(initialVisibilityFilter);
|
|
}, [initialVisibilityFilter]);
|
|
|
|
const handleVisibilityFilterChange = useCallback((value: string) => {
|
|
const newValue = value === 'invisible' || value === 'private' ? value : undefined;
|
|
setVisibilityFilter(newValue);
|
|
if (searchQuery) {
|
|
const params = new URLSearchParams(location.search);
|
|
if (newValue) params.set('visibilityFilter', newValue);
|
|
else params.delete('visibilityFilter');
|
|
navigate(`/search?${params.toString()}`, { replace: true });
|
|
}
|
|
}, [searchQuery, location.search, navigate]);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('feedViewMode', viewMode);
|
|
}, [viewMode]);
|
|
|
|
const categorySlugs = useMemo(() => {
|
|
if (!propCategorySlugs) return undefined;
|
|
return propCategorySlugs.split(',').map(s => s.trim()).filter(Boolean);
|
|
}, [propCategorySlugs]);
|
|
|
|
// Derive source/sourceId for user filtering
|
|
const feedSource = searchQuery ? 'search' as const : propUserId ? 'user' as const : 'home' as const;
|
|
const feedSourceId = searchQuery || propUserId || undefined;
|
|
|
|
// Mobile sheet state
|
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
const closeSheet = useCallback(() => setSheetOpen(false), []);
|
|
|
|
// Persist sidebar size
|
|
const [sidebarSize, setSidebarSize] = useState(() => {
|
|
const stored = localStorage.getItem(SIDEBAR_KEY);
|
|
return stored ? Number(stored) : DEFAULT_SIDEBAR;
|
|
});
|
|
const handleSidebarResize = useCallback((size: number) => {
|
|
setSidebarSize(size);
|
|
localStorage.setItem(SIDEBAR_KEY, String(size));
|
|
}, []);
|
|
|
|
// Navigation helpers — toggle local state
|
|
const handleSortChange = useCallback((value: string) => {
|
|
if (!value) return;
|
|
if (value === 'latest') setSortBy('latest');
|
|
else if (value === 'top') setSortBy('top');
|
|
else if (value === 'categories') setShowCategories(prev => !prev);
|
|
}, []);
|
|
|
|
const handleCategoriesToggle = useCallback(() => {
|
|
setShowCategories(prev => {
|
|
const next = !prev;
|
|
if (next && isMobile) setSheetOpen(true);
|
|
return next;
|
|
});
|
|
}, [isMobile]);
|
|
|
|
// --- Shared sort + categories toggle bar ---
|
|
const renderSortBar = (size?: 'sm') => (
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
<ToggleGroup type="single" value={sortBy} onValueChange={handleSortChange}>
|
|
<ToggleGroupItem value="latest" aria-label="Latest Posts" size={size}>
|
|
<Clock className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Latest</T></span>
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="top" aria-label="Top Posts" size={size}>
|
|
<TrendingUp className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Top</T></span>
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
<ToggleGroup type="single" value={showCategories ? 'categories' : ''} onValueChange={handleCategoriesToggle}>
|
|
<ToggleGroupItem value="categories" aria-label="Show Categories" size={size}>
|
|
<FolderTree className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Categories</T></span>
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
<ToggleGroup type="single" value={contentType || 'all'} onValueChange={(v) => {
|
|
if (!v || v === 'all') handleContentTypeChange(undefined);
|
|
else handleContentTypeChange(v as 'posts' | 'pages' | 'pictures' | 'files');
|
|
}}>
|
|
<ToggleGroupItem value="all" aria-label="All content" size={size}>
|
|
<span className="hidden md:inline"><T>All</T></span>
|
|
<span className="md:hidden text-xs">All</span>
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="posts" aria-label="Posts only" size={size}>
|
|
<ImageIcon className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Posts</T></span>
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="pages" aria-label="Pages only" size={size}>
|
|
<FileText className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Pages</T></span>
|
|
</ToggleGroupItem>
|
|
{isOwnProfile && (
|
|
<ToggleGroupItem value="pictures" aria-label="Pictures only" size={size}>
|
|
<Camera className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Pictures</T></span>
|
|
</ToggleGroupItem>
|
|
)}
|
|
<ToggleGroupItem value="files" aria-label="Files only" size={size}>
|
|
<FolderTree className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Files</T></span>
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
{isOwnProfile && (
|
|
<ToggleGroup type="single" value={visibilityFilter || ''} onValueChange={handleVisibilityFilterChange}>
|
|
<ToggleGroupItem value="invisible" aria-label="Show invisible only" size={size}>
|
|
<EyeOff className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Invisible</T></span>
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="private" aria-label="Show private only" size={size}>
|
|
<Lock className="h-4 w-4 md:mr-2" />
|
|
<span className="hidden md:inline"><T>Private</T></span>
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
// --- Pictures grid (standalone pictures, not posts) ---
|
|
const [pictures, setPictures] = useState<any[]>([]);
|
|
const [picturesLoading, setPicturesLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (contentType !== 'pictures' || !propUserId) return;
|
|
let cancelled = false;
|
|
setPicturesLoading(true);
|
|
import('@/modules/posts/client-pictures').then(({ fetchPictures }) =>
|
|
fetchPictures({ userId: propUserId })
|
|
).then(result => {
|
|
if (!cancelled) setPictures(result.data);
|
|
}).catch(err => console.error('Failed to load pictures', err))
|
|
.finally(() => { if (!cancelled) setPicturesLoading(false); });
|
|
return () => { cancelled = true; };
|
|
}, [contentType, propUserId, refreshKey]);
|
|
|
|
const renderPicturesGrid = () => {
|
|
if (picturesLoading) {
|
|
return <div className="py-8 text-center text-muted-foreground"><T>Loading pictures...</T></div>;
|
|
}
|
|
if (pictures.length === 0) {
|
|
return <div className="py-8 text-center text-muted-foreground"><T>No pictures yet</T></div>;
|
|
}
|
|
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
|
|
const isAuto = columns === 'auto';
|
|
const gridColsClass = isAuto ? 'grid-cols-[repeat(auto-fit,minmax(250px,350px))] justify-center' :
|
|
Number(columns) === 1 ? 'grid-cols-1' :
|
|
Number(columns) === 2 ? 'grid-cols-1 md:grid-cols-2' :
|
|
Number(columns) === 3 ? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' :
|
|
Number(columns) === 5 ? 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5' :
|
|
Number(columns) === 6 ? 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6' :
|
|
'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 lg:grid-cols-5'; // default 4-ish/5
|
|
|
|
return (
|
|
<div className={`grid ${gridColsClass} gap-1 px-1`}>
|
|
{pictures.map(pic => {
|
|
const thumbUrl = pic.thumbnail_url || pic.image_url;
|
|
const optimized = thumbUrl
|
|
? thumbUrl.startsWith('http')
|
|
? thumbUrl
|
|
: `${SERVER_URL}/api/v1/image?url=${encodeURIComponent(thumbUrl)}&w=400&q=75`
|
|
: undefined;
|
|
|
|
return (
|
|
<div key={pic.id} className="relative group">
|
|
<MediaCard
|
|
id={pic.id}
|
|
pictureId={pic.picture_id}
|
|
url={pic.image_url!}
|
|
thumbnailUrl={optimized}
|
|
title={pic.title}
|
|
author={undefined as any}
|
|
authorAvatarUrl={undefined}
|
|
authorId={pic.user_id}
|
|
likes={pic.likes_count || 0}
|
|
comments={0}
|
|
isLiked={false}
|
|
type={normalizeMediaType(pic.type)}
|
|
meta={pic.meta}
|
|
onClick={() => navigate(`/post/${pic.id}`)}
|
|
onLike={() => { }}
|
|
onDelete={() => { }}
|
|
onEdit={() => { }}
|
|
created_at={pic.created_at}
|
|
job={pic.job}
|
|
responsive={pic.responsive}
|
|
apiUrl={SERVER_URL}
|
|
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div >
|
|
);
|
|
};
|
|
|
|
// --- Feed views ---
|
|
const renderFeed = () => {
|
|
// When 'pictures' content type is selected, render the standalone grid
|
|
if (contentType === 'pictures') {
|
|
return renderPicturesGrid();
|
|
}
|
|
|
|
if (isMobile) {
|
|
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}`} />
|
|
);
|
|
}
|
|
|
|
if (viewMode === 'grid') {
|
|
return <PhotoGrid key={refreshKey} navigationSource={feedSource} navigationSourceId={feedSourceId} sortBy={sortBy} showVideos={true} categorySlugs={categorySlugs} categoryIds={propCategoryId ? [propCategoryId] : undefined} contentType={contentType} visibilityFilter={visibilityFilter} center={center} columns={columns} preset={{ showTitle, showDescription }} />;
|
|
} else if (viewMode === 'large') {
|
|
return <GalleryLarge key={refreshKey} navigationSource={feedSource} navigationSourceId={feedSourceId} sortBy={sortBy} categorySlugs={categorySlugs} categoryIds={propCategoryId ? [propCategoryId] : undefined} contentType={contentType} visibilityFilter={visibilityFilter} center={center} preset={{ showTitle, showDescription }} />;
|
|
}
|
|
return <ListLayout key={refreshKey} navigationSource={feedSource} navigationSourceId={feedSourceId} sortBy={sortBy} categorySlugs={categorySlugs} categoryIds={propCategoryId ? [propCategoryId] : undefined} contentType={contentType} visibilityFilter={visibilityFilter} center={center} preset={{ showTitle, showDescription }} />;
|
|
};
|
|
|
|
return (
|
|
<div className={cn("dark:bg-slate-800/50 lg:rounded-lg p-2 md:p-6", center && "container mx-auto max-w-7xl")}>
|
|
<SEO title="PolyMech Home" />
|
|
|
|
{/* Mobile: Sheet for category navigation */}
|
|
{isMobile && (
|
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
<SheetContent side="left" className="w-[280px] p-4">
|
|
<SheetHeader className="mb-2">
|
|
<SheetTitle className="text-sm"><T>Categories</T></SheetTitle>
|
|
<SheetDescription className="sr-only"><T>Browse categories</T></SheetDescription>
|
|
</SheetHeader>
|
|
<div className="overflow-y-auto flex-1">
|
|
<CategoryTreeView onNavigate={closeSheet} filterType="pages" />
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)}
|
|
|
|
<div className="md:py-2">
|
|
{isMobile ? (
|
|
/* ---- Mobile layout ---- */
|
|
<div className="md:hidden">
|
|
<div className="flex justify-between items-center px-1 py-2 bg-muted/40 border-b">
|
|
<div className="flex items-center gap-1">
|
|
{heading && (() => {
|
|
const H = headingLevel || 'h2';
|
|
return <H className="text-sm font-bold truncate max-w-[120px]"><T>{heading}</T></H>;
|
|
})()}
|
|
{showSortBar && (
|
|
<ToggleGroup type="single" value={sortBy} onValueChange={handleSortChange}>
|
|
<ToggleGroupItem value="latest" aria-label="Latest Posts" size="sm">
|
|
<Clock className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="top" aria-label="Top Posts" size="sm">
|
|
<TrendingUp className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)}
|
|
{showSortBar && (
|
|
<ToggleGroup type="single" value={showCategories ? 'categories' : ''} onValueChange={handleCategoriesToggle}>
|
|
<ToggleGroupItem value="categories" aria-label="Show Categories" size="sm">
|
|
<FolderTree className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)}
|
|
</div>
|
|
{categorySlugs && categorySlugs.length > 0 && (
|
|
<span className="flex-1 text-center text-sm font-semibold text-foreground/70 truncate px-2 capitalize">
|
|
{categorySlugs.map(s => s.replace(/-/g, ' ')).join(', ')}
|
|
</span>
|
|
)}
|
|
<div className="flex items-center gap-1">
|
|
{showLayoutToggles && (
|
|
<ToggleGroup type="single" value={viewMode === 'list' ? 'list' : 'grid'} onValueChange={(v) => v && setViewMode(v as any)}>
|
|
<ToggleGroupItem value="grid" aria-label="Card View" size="sm">
|
|
<LayoutGrid className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="list" aria-label="List View" size="sm">
|
|
<List className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)}
|
|
{showSortBar && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 relative">
|
|
<SlidersHorizontal className="h-4 w-4" />
|
|
{(contentType || visibilityFilter) && (
|
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-primary" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-44">
|
|
<DropdownMenuLabel className="text-xs"><T>Content</T></DropdownMenuLabel>
|
|
<DropdownMenuRadioGroup value={contentType || 'all'} onValueChange={(v) => {
|
|
if (v === 'all') handleContentTypeChange(undefined);
|
|
else handleContentTypeChange(v as 'posts' | 'pages' | 'pictures');
|
|
}}>
|
|
<DropdownMenuRadioItem value="all">
|
|
<Layers className="h-4 w-4 mr-2" /><T>All</T>
|
|
</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="posts">
|
|
<ImageIcon className="h-4 w-4 mr-2" /><T>Posts</T>
|
|
</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="pages">
|
|
<FileText className="h-4 w-4 mr-2" /><T>Pages</T>
|
|
</DropdownMenuRadioItem>
|
|
{isOwnProfile && (
|
|
<DropdownMenuRadioItem value="pictures">
|
|
<Camera className="h-4 w-4 mr-2" /><T>Pictures</T>
|
|
</DropdownMenuRadioItem>
|
|
)}
|
|
<DropdownMenuRadioItem value="files">
|
|
<FolderTree className="h-4 w-4 mr-2" /><T>Files</T>
|
|
</DropdownMenuRadioItem>
|
|
</DropdownMenuRadioGroup>
|
|
{isOwnProfile && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuLabel className="text-xs"><T>Visibility</T></DropdownMenuLabel>
|
|
<DropdownMenuRadioGroup value={visibilityFilter || ''} onValueChange={handleVisibilityFilterChange}>
|
|
<DropdownMenuRadioItem value="">
|
|
<Layers className="h-4 w-4 mr-2" /><T>All</T>
|
|
</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="invisible">
|
|
<EyeOff className="h-4 w-4 mr-2" /><T>Invisible</T>
|
|
</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="private">
|
|
<Lock className="h-4 w-4 mr-2" /><T>Private</T>
|
|
</DropdownMenuRadioItem>
|
|
</DropdownMenuRadioGroup>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{renderFeed()}
|
|
</div>
|
|
) : showCategories ? (
|
|
/* ---- Desktop with category sidebar ---- */
|
|
<div className="hidden md:block">
|
|
<div className="flex justify-between items-center px-4 mb-4">
|
|
<div className="flex items-center gap-3">
|
|
{heading && (() => {
|
|
const H = headingLevel || 'h2';
|
|
const cls = H === 'h1' ? 'text-2xl' : H === 'h2' ? 'text-xl' : H === 'h3' ? 'text-lg' : 'text-base';
|
|
return <H className={`${cls} font-bold text-foreground max-w-[50vw] truncate`}><T>{heading}</T></H>;
|
|
})()}
|
|
{showSortBar && renderSortBar()}
|
|
</div>
|
|
{showLayoutToggles && (
|
|
<ToggleGroup type="single" value={viewMode} onValueChange={(v) => v && setViewMode(v as any)}>
|
|
<ToggleGroupItem value="grid" aria-label="Grid View">
|
|
<LayoutGrid className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="large" aria-label="Large View">
|
|
<GalleryVerticalEnd className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="list" aria-label="List View">
|
|
<List className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)}
|
|
</div>
|
|
|
|
<ResizablePanelGroup direction="horizontal" className="min-h-[calc(100vh-8rem)]">
|
|
<ResizablePanel
|
|
defaultSize={sidebarSize}
|
|
minSize={10}
|
|
maxSize={25}
|
|
onResize={handleSidebarResize}
|
|
>
|
|
<div className="h-full overflow-y-auto border-r px-2">
|
|
<div className="sticky top-0 bg-background/95 backdrop-blur-sm pb-1 pt-1 px-1 border-b mb-1">
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide"><T>Categories</T></span>
|
|
</div>
|
|
<CategoryTreeView filterType="pages" />
|
|
</div>
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel defaultSize={100 - sidebarSize}>
|
|
{renderFeed()}
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
) : (
|
|
/* ---- Desktop without sidebar ---- */
|
|
<div className="hidden md:block">
|
|
<div className="flex justify-between items-center px-4 mb-4">
|
|
<div className="flex items-center gap-3">
|
|
{heading && (() => {
|
|
const H = headingLevel || 'h2';
|
|
const cls = H === 'h1' ? 'text-2xl' : H === 'h2' ? 'text-xl' : H === 'h3' ? 'text-lg' : 'text-base';
|
|
return <H className={`${cls} font-bold text-foreground max-w-[50vw] truncate`}><T>{heading}</T></H>;
|
|
})()}
|
|
{showSortBar && renderSortBar()}
|
|
</div>
|
|
{showLayoutToggles && (
|
|
<ToggleGroup type="single" value={viewMode} onValueChange={(v) => v && setViewMode(v as any)}>
|
|
<ToggleGroupItem value="grid" aria-label="Grid View">
|
|
<LayoutGrid className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="large" aria-label="Large View">
|
|
<GalleryVerticalEnd className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="list" aria-label="List View">
|
|
<List className="h-4 w-4" />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)}
|
|
</div>
|
|
{renderFeed()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{showFooter && <Footer />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HomeWidget;
|