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 (
-