From 6dad7aafbf4aa5ddd9817455ff646089c9491383 Mon Sep 17 00:00:00 2001 From: Babayaga Date: Thu, 9 Apr 2026 00:11:32 +0200 Subject: [PATCH] search:places --- packages/ui/src/components/GalleryLarge.tsx | 2 +- packages/ui/src/components/ListLayout.tsx | 24 +++++++++++++-- packages/ui/src/components/MediaCard.tsx | 30 +++++++++++++++++++ packages/ui/src/components/PhotoCard.tsx | 14 +++++++-- packages/ui/src/components/PhotoGrid.tsx | 5 ++-- packages/ui/src/components/feed/FeedCard.tsx | 2 +- .../ui/src/components/feed/MobileFeed.tsx | 5 ++-- .../ui/src/components/widgets/HomeWidget.tsx | 23 ++++++++++---- packages/ui/src/hooks/useFeedData.ts | 2 +- packages/ui/src/modules/feed/client-feed.ts | 2 +- packages/ui/src/modules/posts/client-posts.ts | 4 +-- .../ui/src/modules/search/client-search.ts | 2 +- packages/ui/src/pages/SearchResults.tsx | 23 +++++++++++--- packages/ui/src/types.ts | 1 + 14 files changed, 113 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/components/GalleryLarge.tsx b/packages/ui/src/components/GalleryLarge.tsx index 70a01883..3254f193 100644 --- a/packages/ui/src/components/GalleryLarge.tsx +++ b/packages/ui/src/components/GalleryLarge.tsx @@ -21,7 +21,7 @@ interface GalleryLargeProps { sortBy?: FeedSortOption; categorySlugs?: string[]; categoryIds?: string[]; - contentType?: 'posts' | 'pages' | 'pictures' | 'files'; + contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; visibilityFilter?: 'invisible' | 'private'; center?: boolean; preset?: any; diff --git a/packages/ui/src/components/ListLayout.tsx b/packages/ui/src/components/ListLayout.tsx index 4225d8bd..2063cb0f 100644 --- a/packages/ui/src/components/ListLayout.tsx +++ b/packages/ui/src/components/ListLayout.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import { useFeedData, FeedSortOption } from "@/hooks/useFeedData"; import { useIsMobile } from "@/hooks/use-mobile"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Link } from "react-router-dom"; import { formatDistanceToNow } from "date-fns"; import { MessageCircle, Heart, ExternalLink } from "lucide-react"; import UserAvatarBlock from "@/components/UserAvatarBlock"; @@ -19,7 +19,7 @@ interface ListLayoutProps { isOwner?: boolean; // Not strictly used for rendering list but good for consistency categorySlugs?: string[]; categoryIds?: string[]; - contentType?: 'posts' | 'pages' | 'pictures' | 'files'; + contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; visibilityFilter?: 'invisible' | 'private'; center?: boolean; preset?: any; @@ -106,6 +106,7 @@ const ListItem = ({ item, isSelected, onClick, preset }: { item: any, isSelected const getSearchGroup = (post: any): string => { if (post.type === 'page-vfs-folder') return 'Folders'; if (post._searchSource === 'picture') return 'Pictures'; + if (post._searchSource === 'place') return 'Places'; if (post._searchSource === 'file') { if (post.thumbnail_url || post.cover || (post.pictures && post.pictures.length > 0)) return 'Pictures'; return 'Files'; @@ -238,7 +239,7 @@ export const ListLayout = ({ groups.get(group)!.push(post); } - const orderedGroups = ['Pages', 'Folders', 'Posts', 'Pictures', 'Files']; + const orderedGroups = ['Pages', 'Folders', 'Posts', 'Places', 'Pictures', 'Files']; const elements: React.ReactNode[] = []; for (const group of orderedGroups) { @@ -311,6 +312,23 @@ export const ListLayout = ({ ); } + if (postAny?.type === 'place-search' && postAny.meta?.url) { + return ( +
+

{postAny.title}

+ {postAny.description && ( +

{postAny.description}

+ )} + + Open place details + +
+ ); + } + return ( Loading...}> = ({ } } + if (normalizedType === MEDIA_TYPES.PLACE_SEARCH) { + return ( +
onClick?.(id)} + className={`group relative overflow-hidden bg-card transition-all duration-300 cursor-pointer w-full ${preset?.showTitle ? '' : 'md:aspect-square'} flex flex-col border rounded-lg hover:border-primary/50`} + > +
+ +
+ {(preset?.showTitle !== false || preset?.showDescription !== false) && ( +
+ {preset?.showTitle !== false && title && ( +

+ {title} +

+ )} + {meta?.placeType && ( +

{meta.placeType}

+ )} + {preset?.showDescription !== false && description && ( +

{description}

+ )} +
+ )} +
+ ); + } + if (normalizedType === MEDIA_TYPES.PAGE || normalizedType === MEDIA_TYPES.PAGE_EXTERNAL || normalizedType === 'page-vfs-file' || @@ -242,6 +271,7 @@ const MediaCard: React.FC = ({ showDescription={showDescription} showAuthor={showAuthor} showActions={showActions} + placeTypeLabel={meta?.placeType} /> ); }; diff --git a/packages/ui/src/components/PhotoCard.tsx b/packages/ui/src/components/PhotoCard.tsx index eec532b2..2791848e 100644 --- a/packages/ui/src/components/PhotoCard.tsx +++ b/packages/ui/src/components/PhotoCard.tsx @@ -50,6 +50,8 @@ interface PhotoCardProps { showActions?: boolean; showTitle?: boolean; showDescription?: boolean; + /** Place search: types[0], shown above description */ + placeTypeLabel?: string | null; } const PhotoCard = ({ @@ -83,7 +85,8 @@ const PhotoCard = ({ showAuthor = true, showActions = true, showTitle = true, - showDescription = true + showDescription = true, + placeTypeLabel }: PhotoCardProps) => { const { user } = useAuth(); const navigate = useNavigate(); @@ -320,11 +323,14 @@ const PhotoCard = ({ - {variant === 'grid' && (title || description) && ( + {variant === 'grid' && (title || description || placeTypeLabel) && (
{showTitle && title && !isLikelyFilename(title) && (

{title}

)} + {placeTypeLabel && ( +

{placeTypeLabel}

+ )} {showDescription && description && (

{description}

)} @@ -565,6 +571,10 @@ const PhotoCard = ({
{title}
)} + {placeTypeLabel && ( +
{placeTypeLabel}
+ )} + {showDescription && description && (
diff --git a/packages/ui/src/components/PhotoGrid.tsx b/packages/ui/src/components/PhotoGrid.tsx index 8b2b84aa..7b0937f2 100644 --- a/packages/ui/src/components/PhotoGrid.tsx +++ b/packages/ui/src/components/PhotoGrid.tsx @@ -57,7 +57,7 @@ interface MediaGridProps { categorySlugs?: string[]; categoryIds?: string[]; preset?: CardPreset; - contentType?: 'posts' | 'pages' | 'pictures' | 'files'; + contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; visibilityFilter?: 'invisible' | 'private'; center?: boolean; columns?: number | 'auto'; @@ -356,6 +356,7 @@ const MediaGrid = ({ const getSearchGroup = (item: any): string => { if (item.type === 'page-vfs-folder') return 'Folders'; if (item._searchSource === 'picture') return 'Pictures'; + if (item._searchSource === 'place') return 'Places'; if (item._searchSource === 'file') { if (item.thumbnail_url || item.cover || (item.pictures && item.pictures.length > 0)) return 'Pictures'; return 'Files'; @@ -374,7 +375,7 @@ const MediaGrid = ({ groups.get(group)!.push(item); }); - const orderedGroups = ['Pages', 'Folders', 'Posts', 'Pictures', 'Files']; + const orderedGroups = ['Pages', 'Folders', 'Posts', 'Places', 'Pictures', 'Files']; const sections = []; for (const group of orderedGroups) { if (groups.has(group)) { diff --git a/packages/ui/src/components/feed/FeedCard.tsx b/packages/ui/src/components/feed/FeedCard.tsx index b4cc5ddf..254dd167 100644 --- a/packages/ui/src/components/feed/FeedCard.tsx +++ b/packages/ui/src/components/feed/FeedCard.tsx @@ -93,7 +93,7 @@ export const FeedCard: React.FC = ({ }; if (carouselItems.length === 0) { - if (post.type === 'page-vfs-file' || post.type === 'page-vfs-folder' || post.type === 'page-intern' || post.type === 'page-external') { + if (post.type === 'page-vfs-file' || post.type === 'page-vfs-folder' || post.type === 'page-intern' || post.type === 'page-external' || post.type === 'place-search') { return (
{ diff --git a/packages/ui/src/components/feed/MobileFeed.tsx b/packages/ui/src/components/feed/MobileFeed.tsx index 9f895b80..4614c508 100644 --- a/packages/ui/src/components/feed/MobileFeed.tsx +++ b/packages/ui/src/components/feed/MobileFeed.tsx @@ -17,7 +17,7 @@ interface MobileFeedProps { sortBy?: FeedSortOption; categorySlugs?: string[]; categoryIds?: string[]; - contentType?: 'posts' | 'pages' | 'pictures' | 'files'; + contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; visibilityFilter?: 'invisible' | 'private'; center?: boolean; showTitle?: boolean; @@ -151,6 +151,7 @@ export const MobileFeed: React.FC = ({ const getSearchGroup = (item: any): string => { if (item.type === 'page-vfs-folder') return 'Folders'; if (item._searchSource === 'picture') return 'Pictures'; + if (item._searchSource === 'place') return 'Places'; if (item._searchSource === 'file') { if (item.thumbnail_url || item.cover || (item.pictures && item.pictures.length > 0)) return 'Pictures'; return 'Files'; @@ -167,7 +168,7 @@ export const MobileFeed: React.FC = ({ groups.get(group)!.push(post); } - const orderedGroups = ['Pages', 'Folders', 'Posts', 'Pictures', 'Files']; + const orderedGroups = ['Pages', 'Folders', 'Posts', 'Places', 'Pictures', 'Files']; const elements: React.ReactNode[] = []; for (const group of orderedGroups) { diff --git a/packages/ui/src/components/widgets/HomeWidget.tsx b/packages/ui/src/components/widgets/HomeWidget.tsx index e39302bb..8f82122a 100644 --- a/packages/ui/src/components/widgets/HomeWidget.tsx +++ b/packages/ui/src/components/widgets/HomeWidget.tsx @@ -20,7 +20,7 @@ export interface HomeWidgetProps { headingLevel?: 'h1' | 'h2' | 'h3' | 'h4'; variables?: Record; searchQuery?: string; - initialContentType?: 'posts' | 'pages' | 'pictures' | 'files'; + initialContentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; initialVisibilityFilter?: 'invisible' | 'private'; } import type { FeedSortOption } from '@/hooks/useFeedData'; @@ -42,7 +42,7 @@ 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 { LayoutGrid, GalleryVerticalEnd, TrendingUp, Clock, List, FolderTree, FileText, Image as ImageIcon, EyeOff, Lock, SlidersHorizontal, Layers, Camera, MapPin } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; @@ -105,7 +105,7 @@ const HomeWidget: React.FC = ({ useEffect(() => { setViewMode(propViewMode); }, [propViewMode]); // Content type filter - const [contentType, setContentType] = useState<'posts' | 'pages' | 'pictures' | 'files' | undefined>( + const [contentType, setContentType] = useState<'posts' | 'pages' | 'pictures' | 'files' | 'places' | undefined>( String(effectiveInitial) === 'all' ? undefined : (effectiveInitial as any) ); @@ -115,7 +115,7 @@ const HomeWidget: React.FC = ({ }, [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) => { + const handleContentTypeChange = useCallback((newType: 'posts' | 'pages' | 'pictures' | 'files' | 'places' | undefined) => { setContentType(newType); if (propUserId) { const basePath = `/user/${propUserId}`; @@ -211,7 +211,7 @@ const HomeWidget: React.FC = ({ { if (!v || v === 'all') handleContentTypeChange(undefined); - else handleContentTypeChange(v as 'posts' | 'pages' | 'pictures' | 'files'); + else handleContentTypeChange(v as 'posts' | 'pages' | 'pictures' | 'files' | 'places'); }}> All @@ -225,6 +225,12 @@ const HomeWidget: React.FC = ({ Pages + {searchQuery && ( + + + Places + + )} {isOwnProfile && ( @@ -428,7 +434,7 @@ const HomeWidget: React.FC = ({ Content { if (v === 'all') handleContentTypeChange(undefined); - else handleContentTypeChange(v as 'posts' | 'pages' | 'pictures'); + else handleContentTypeChange(v as 'posts' | 'pages' | 'pictures' | 'files' | 'places'); }}> All @@ -439,6 +445,11 @@ const HomeWidget: React.FC = ({ Pages + {searchQuery && ( + + Places + + )} {isOwnProfile && ( Pictures diff --git a/packages/ui/src/hooks/useFeedData.ts b/packages/ui/src/hooks/useFeedData.ts index 09f1f386..1f320070 100644 --- a/packages/ui/src/hooks/useFeedData.ts +++ b/packages/ui/src/hooks/useFeedData.ts @@ -22,7 +22,7 @@ interface UseFeedDataProps { supabaseClient?: any; categoryIds?: string[]; categorySlugs?: string[]; - contentType?: 'posts' | 'pages' | 'pictures' | 'files'; + contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; visibilityFilter?: 'invisible' | 'private'; } diff --git a/packages/ui/src/modules/feed/client-feed.ts b/packages/ui/src/modules/feed/client-feed.ts index f7364595..8d185c96 100644 --- a/packages/ui/src/modules/feed/client-feed.ts +++ b/packages/ui/src/modules/feed/client-feed.ts @@ -11,7 +11,7 @@ export interface FetchFeedOptions { sortBy?: FeedSortOption; categoryIds?: string[]; categorySlugs?: string[]; - contentType?: 'posts' | 'pages' | 'pictures' | 'files'; + contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; visibilityFilter?: 'invisible' | 'private'; lang?: string; } diff --git a/packages/ui/src/modules/posts/client-posts.ts b/packages/ui/src/modules/posts/client-posts.ts index c2a56182..9ef69317 100644 --- a/packages/ui/src/modules/posts/client-posts.ts +++ b/packages/ui/src/modules/posts/client-posts.ts @@ -178,14 +178,14 @@ export const mapFeedPostsToMediaItems = (posts: FeedPost[], sortBy: 'latest' | ' if (!cover) { // Support items without covers that should still be displayed - const allowedWithoutCover = ['page-vfs-folder', 'page-vfs-file', 'page-external', 'page-intern', 'page-github']; + const allowedWithoutCover = ['page-vfs-folder', 'page-vfs-file', 'page-external', 'page-intern', 'page-github', 'place-search']; if (allowedWithoutCover.includes(post.type)) { return { id: post.id, picture_id: post.id, title: post.title, description: post.description, - image_url: post.meta?.url || '', + image_url: post.type === 'place-search' ? '' : (post.meta?.url || ''), thumbnail_url: null, type: post.type as MediaType, meta: post.meta, diff --git a/packages/ui/src/modules/search/client-search.ts b/packages/ui/src/modules/search/client-search.ts index 02808dd0..30bd339c 100644 --- a/packages/ui/src/modules/search/client-search.ts +++ b/packages/ui/src/modules/search/client-search.ts @@ -3,7 +3,7 @@ const SERVER_API_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; export interface SearchOptions { q: string; limit?: number; - type?: 'all' | 'pages' | 'posts' | 'pictures' | 'files'; + type?: 'all' | 'pages' | 'posts' | 'pictures' | 'files' | 'places'; sizes?: string; formats?: string; token?: string; diff --git a/packages/ui/src/pages/SearchResults.tsx b/packages/ui/src/pages/SearchResults.tsx index a4a196ec..4c482645 100644 --- a/packages/ui/src/pages/SearchResults.tsx +++ b/packages/ui/src/pages/SearchResults.tsx @@ -26,7 +26,7 @@ const SearchResults = () => { const columns = columnsParam === 'auto' ? 'auto' : (columnsParam ? parseInt(columnsParam, 10) : 4); const heading = searchParams.get('heading') || undefined; const headingLevel = (searchParams.get('headingLevel') as 'h1' | 'h2' | 'h3' | 'h4') || undefined; - const initialContentType = (searchParams.get('type') || searchParams.get('initialContentType')) as 'posts' | 'pages' | 'pictures' | 'files' | undefined; + const initialContentType = (searchParams.get('type') || searchParams.get('initialContentType')) as 'posts' | 'pages' | 'pictures' | 'files' | 'places' | undefined; const visibilityFilter = (searchParams.get('visibilityFilter')) as 'invisible' | 'private' | undefined; // Sync input with URL query @@ -68,7 +68,7 @@ const SearchResults = () => { setInputQuery(e.target.value)} @@ -79,7 +79,7 @@ const SearchResults = () => {

Enter a search term

-

Search for pages, posts, and pictures

+

Search for pages, posts, pictures, and places

@@ -87,7 +87,22 @@ const SearchResults = () => { } return ( -
+
+
+
+ + setInputQuery(e.target.value)} + onKeyDown={handleSearchKeyDown} + aria-label="Search" + /> + +