From 184713dba80ce0e09c6a87632e55b70d8bbb3fce Mon Sep 17 00:00:00 2001 From: lizhixuan Date: Mon, 12 May 2025 22:58:54 +0800 Subject: [PATCH] feat: enhance store page with new components and functionality - Updated component paths in components.json for better organization. - Added 'motion' library to package.json for animations. - Refactored TypeScript configuration to include new renderer paths. - Implemented new StoreContent and StoreSidebar components for improved store layout. - Integrated store categories and items with filtering capabilities. - Enhanced UI with Tailwind CSS animations and styles for a better user experience. --- components.json | 2 +- package.json | 1 + src/renderer/src/assets/styles/index.scss | 2 +- src/renderer/src/assets/styles/tailwind.css | 20 +- src/renderer/src/components/app/Sidebar.tsx | 4 +- .../pages/store/components/StoreContent.tsx | 207 ++++++++++++ .../pages/store/components/StoreSidebar.tsx | 57 ++++ .../pages/store}/data/store_categories.json | 0 .../src/pages/store}/data/store_list.json | 0 src/renderer/src/pages/store/index.tsx | 302 +++--------------- src/renderer/src/ui/alert.tsx | 66 ++++ src/renderer/src/ui/third-party/BlurFade.tsx | 73 +++++ tsconfig.node.json | 4 +- yarn.lock | 60 ++++ 14 files changed, 534 insertions(+), 264 deletions(-) create mode 100644 src/renderer/src/pages/store/components/StoreContent.tsx create mode 100644 src/renderer/src/pages/store/components/StoreSidebar.tsx rename {resources => src/renderer/src/pages/store}/data/store_categories.json (100%) rename {resources => src/renderer/src/pages/store}/data/store_list.json (100%) create mode 100644 src/renderer/src/ui/alert.tsx create mode 100644 src/renderer/src/ui/third-party/BlurFade.tsx diff --git a/components.json b/components.json index 283cb015f5..3d64f95424 100644 --- a/components.json +++ b/components.json @@ -11,7 +11,7 @@ "prefix": "" }, "aliases": { - "components": "@renderer", + "components": "@renderer/ui/third-party", "utils": "@renderer/utils", "ui": "@renderer/ui", "lib": "@renderer/lib", diff --git a/package.json b/package.json index 1e3ca06b95..8694b7210e 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,7 @@ "lru-cache": "^11.1.0", "lucide-react": "^0.509.0", "mime": "^4.0.4", + "motion": "^12.11.0", "npx-scope-finder": "^1.2.0", "openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch", "p-queue": "^8.1.0", diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 9721f36126..baf9bda8c6 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -136,7 +136,7 @@ body[theme-mode='light'] { *::before, *::after { box-sizing: border-box; - margin: 0; + // margin: 0; font-weight: normal; } diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css index f25c11060c..f161b48776 100644 --- a/src/renderer/src/assets/styles/tailwind.css +++ b/src/renderer/src/assets/styles/tailwind.css @@ -109,6 +109,24 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } + } + @keyframes marquee-vertical { + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } + } } @layer base { @@ -118,4 +136,4 @@ body { @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 7f58de9f5f..60fbe2d608 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -11,8 +11,8 @@ import { isEmoji } from '@renderer/utils' import type { MenuProps } from 'antd' import { Avatar, Dropdown, Tooltip } from 'antd' import { - Box, CircleHelp, + Compass, FileSearch, Folder, Languages, @@ -145,7 +145,7 @@ const MainMenus: FC = () => { minapp: , knowledge: , files: , - store: + store: } const pathMap = { diff --git a/src/renderer/src/pages/store/components/StoreContent.tsx b/src/renderer/src/pages/store/components/StoreContent.tsx new file mode 100644 index 0000000000..4bd11e03f1 --- /dev/null +++ b/src/renderer/src/pages/store/components/StoreContent.tsx @@ -0,0 +1,207 @@ +import { Badge } from '@renderer/ui/badge' +import { Button } from '@renderer/ui/button' +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@renderer/ui/card' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@renderer/ui/dropdown-menu' +import { Input } from '@renderer/ui/input' +import { BlurFade } from '@renderer/ui/third-party/BlurFade' +import { cn } from '@renderer/utils' +import { Download, Filter, Grid3X3, List, Search, Star } from 'lucide-react' +// Define the type for a store item based on store_list.json +interface StoreItem { + id: number + title: string + description: string + type: string + categoryId: string + subcategoryId: string + author: string + rating: number + downloads: string + image?: string + tags: string[] + featured: boolean +} + +interface StoreContentProps { + viewMode: 'grid' | 'list' + searchQuery: string + selectedCategory: string + items: StoreItem[] + onSearchQueryChange: (query: string) => void + onViewModeChange: (mode: 'grid' | 'list') => void +} + +export function StoreContent({ + viewMode, + searchQuery, + items, + onSearchQueryChange, + onViewModeChange +}: StoreContentProps) { + return ( +
+ {/* Sticky Header for Search, Filter, View Mode, and Category Tabs */} +
+
+ {/* Top row: Search, Filter, View buttons */} +
+
+ + onSearchQueryChange(e.target.value)} + /> +
+ + + + + + {/* Add actual filtering logic later */} + Most Popular + Newest + Highest Rated + + + + +
+ + {/* Bottom row: Category Tabs */} + {/* + + All + MCP Services + Plugins + Applications + + */} +
+
+ + {/* Main Content Area: Grid or List View */} +
+ {items.length === 0 ? ( +
+

No items found matching your criteria.

+
+ ) : viewMode === 'grid' ? ( + + ) : ( + + )} +
+
+ ) +} + +// Grid View Component +function GridView({ items }: { items: StoreItem[] }) { + return ( +
+ {items.map((item, idx) => ( + + + +
+ {item.title} +
+
+ +
+
+ {item.title} +

{item.author}

+
+ {item.type} +
+

{item.description}

+
+ +
+ + {item.rating} +
+ +
+
+
+ ))} +
+ ) +} + +// List View Component +function ListView({ items }: { items: StoreItem[] }) { + return ( +
+ {items.map((item) => ( + +
+
+ {item.title} +
+
+
+
+
+

{item.title}

+

{item.author}

+
+ {item.type} +
+

{item.description}

+
+ {item.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+
+ + {item.rating} + ({item.downloads}) +
+ +
+
+
+ ))} +
+ ) +} diff --git a/src/renderer/src/pages/store/components/StoreSidebar.tsx b/src/renderer/src/pages/store/components/StoreSidebar.tsx new file mode 100644 index 0000000000..6898f62bb0 --- /dev/null +++ b/src/renderer/src/pages/store/components/StoreSidebar.tsx @@ -0,0 +1,57 @@ +import { Badge } from '@renderer/ui/badge' +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem +} from '@renderer/ui/sidebar' + +import categories from '../data/store_categories.json' + +interface StoreSidebarProps { + selectedCategory: string + selectedSubcategory: string + onSelectCategory: (categoryId: string, subcategoryId: string) => void +} + +export function StoreSidebar({ selectedCategory, selectedSubcategory, onSelectCategory }: StoreSidebarProps) { + console.log('selectedCategory', selectedCategory) + console.log('selectedSubcategory', selectedSubcategory) + return ( + + + {categories.map((category) => ( + + {/* Only render label if it's not the 'all' category wrapper */} + {category.id !== 'all' && {category.title}} + + + {category.items.map((item) => ( + + { + console.log('category', category) + // Special handling for top-level 'all' categories vs nested ones + onSelectCategory(category.id, item.id) // Selecting 'all', 'featured', 'new', 'top' uses the item.id as category + }}> + {item.name} + + {item.count} + + + + ))} + + + + ))} + + + ) +} diff --git a/resources/data/store_categories.json b/src/renderer/src/pages/store/data/store_categories.json similarity index 100% rename from resources/data/store_categories.json rename to src/renderer/src/pages/store/data/store_categories.json diff --git a/resources/data/store_list.json b/src/renderer/src/pages/store/data/store_list.json similarity index 100% rename from resources/data/store_list.json rename to src/renderer/src/pages/store/data/store_list.json diff --git a/src/renderer/src/pages/store/index.tsx b/src/renderer/src/pages/store/index.tsx index 43db372b7a..9af45df52d 100644 --- a/src/renderer/src/pages/store/index.tsx +++ b/src/renderer/src/pages/store/index.tsx @@ -1,72 +1,11 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' -import { Badge } from '@renderer/ui/badge' -import { Button } from '@renderer/ui/button' -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@renderer/ui/card' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@renderer/ui/dropdown-menu' -import { Input } from '@renderer/ui/input' -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarProvider -} from '@renderer/ui/sidebar' -import { Tabs, TabsList, TabsTrigger } from '@renderer/ui/tabs' -import { cn } from '@renderer/utils' -import { Download, Filter, Grid3X3, List, Search, Star } from 'lucide-react' +import { SidebarProvider } from '@renderer/ui/sidebar' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import categories from '../../../../../resources/data/store_categories.json' -import storeList from '../../../../../resources/data/store_list.json' - -// const categories = [ -// { -// id: 'all', -// title: 'Categories', -// items: [ -// { id: 'all', name: 'All', count: 120, isActive: true }, -// { id: 'featured', name: 'Featured', count: 24 }, -// { id: 'new', name: 'New Releases', count: 18 }, -// { id: 'top', name: 'Top Rated', count: 32 } -// ] -// }, -// { -// id: 'mcp', -// title: 'MCP Services', -// items: [ -// { id: 'mcp-text', name: 'Text Generation', count: 15 }, -// { id: 'mcp-image', name: 'Image Generation', count: 8 }, -// { id: 'mcp-audio', name: 'Audio Processing', count: 6 }, -// { id: 'mcp-code', name: 'Code Assistance', count: 12 } -// ] -// }, -// { -// id: 'plugins', -// title: 'Plugins', -// items: [ -// { id: 'plugin-productivity', name: 'Productivity', count: 14 }, -// { id: 'plugin-development', name: 'Development', count: 22 }, -// { id: 'plugin-design', name: 'Design', count: 9 }, -// { id: 'plugin-utilities', name: 'Utilities', count: 18 } -// ] -// }, -// { -// id: 'apps', -// title: 'Applications', -// items: [ -// { id: 'app-desktop', name: 'Desktop', count: 7 }, -// { id: 'app-web', name: 'Web', count: 11 }, -// { id: 'app-mobile', name: 'Mobile', count: 5 }, -// { id: 'app-cli', name: 'CLI', count: 8 } -// ] -// } -// ] +import { StoreContent } from './components/StoreContent' +import { StoreSidebar } from './components/StoreSidebar' +import storeList from './data/store_list.json' export default function StoreLayout() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') @@ -74,9 +13,8 @@ export default function StoreLayout() { const [selectedCategory, setSelectedCategory] = useState('all') const [selectedSubcategory, setSelectedSubcategory] = useState('all') const { t } = useTranslation() - // Update the filteredItems logic to use the new category IDs + const filteredItems = storeList.filter((item) => { - // First apply search filter const matchesSearch = searchQuery === '' || item.title.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -84,17 +22,38 @@ export default function StoreLayout() { item.author.toLowerCase().includes(searchQuery.toLowerCase()) || item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) - // Then apply category filters - const matchesCategory = - selectedCategory === 'all' || - item.categoryId === selectedCategory || - (selectedCategory === 'featured' && item.featured) + let matchesCategory = false + if (selectedCategory === 'all') { + matchesCategory = true + } else if (['featured', 'new', 'top'].includes(selectedCategory)) { + if (selectedCategory === 'featured') { + matchesCategory = item.featured === true + } + } else { + matchesCategory = item.categoryId === selectedCategory + } - const matchesSubcategory = selectedSubcategory === 'all' || item.subcategoryId === selectedSubcategory + const matchesSubcategory = + ['all', 'featured', 'new', 'top'].includes(selectedCategory) || + selectedSubcategory === 'all' || + item.subcategoryId === selectedSubcategory return matchesSearch && matchesCategory && matchesSubcategory }) + const handleSelectCategory = (categoryId: string, subcategoryId: string) => { + console.log('categoryId', categoryId) + console.log('subcategoryId', subcategoryId) + setSelectedCategory(categoryId) + setSelectedSubcategory(subcategoryId) + // setSelectedSubcategory('all') + } + + // const handleTabCategoryChange = (categoryId: string) => { + // setSelectedCategory(categoryId) + // setSelectedSubcategory('all') + // } + return (
@@ -102,190 +61,19 @@ export default function StoreLayout() {
- - -
-

Cherry Store

-
-
- - {categories.map((category) => ( - - {category.title} - - - {category.items.map((item) => ( - - { - setSelectedCategory(category.id) - setSelectedSubcategory(item.id) - }}> - {item.name} - - {item.count} - - - - ))} - - - - ))} - -
- -
-
-
-
-
- - setSearchQuery(e.target.value)} - /> -
- - - - - - Most Popular - Newest - Highest Rated - - - - -
- - { - setSelectedCategory(value) - setSelectedSubcategory('all') - }}> - - All - MCP Services - Plugins - Applications - - -
-
- -
- {filteredItems.length === 0 ? ( -
-

No items found matching your search.

-
- ) : viewMode === 'grid' ? ( -
- {filteredItems.map((item) => ( - - -
- {item.title} -
-
- -
-
- {item.title} -

{item.author}

-
- {item.type} -
-

{item.description}

-
- -
- - {item.rating} -
- -
-
- ))} -
- ) : ( -
- {filteredItems.map((item) => ( - -
-
- {item.title} -
-
-
-
-
-

{item.title}

-

{item.author}

-
- {item.type} -
-

{item.description}

-
- {item.tags.map((tag) => ( - - {tag} - - ))} -
-
-
-
-
- - {item.rating} - ({item.downloads}) -
- -
-
-
- ))} -
- )} -
-
+ +
diff --git a/src/renderer/src/ui/alert.tsx b/src/renderer/src/ui/alert.tsx new file mode 100644 index 0000000000..7d0a2918d6 --- /dev/null +++ b/src/renderer/src/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@renderer/utils/index" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/renderer/src/ui/third-party/BlurFade.tsx b/src/renderer/src/ui/third-party/BlurFade.tsx new file mode 100644 index 0000000000..ae5930cac1 --- /dev/null +++ b/src/renderer/src/ui/third-party/BlurFade.tsx @@ -0,0 +1,73 @@ +'use client' + +import { AnimatePresence, motion, MotionProps, useInView, UseInViewOptions, Variants } from 'motion/react' +import { useRef } from 'react' + +type MarginType = UseInViewOptions['margin'] + +interface BlurFadeProps extends MotionProps { + children: React.ReactNode + className?: string + variant?: { + hidden: { y: number } + visible: { y: number } + } + duration?: number + delay?: number + offset?: number + direction?: 'up' | 'down' | 'left' | 'right' + inView?: boolean + inViewMargin?: MarginType + blur?: string +} + +export function BlurFade({ + children, + className, + variant, + duration = 0.4, + delay = 0, + offset = 6, + direction = 'down', + inView = false, + inViewMargin = '-50px', + blur = '6px', + ...props +}: BlurFadeProps) { + const ref = useRef(null) + const inViewResult = useInView(ref, { once: true, margin: inViewMargin }) + const isInView = !inView || inViewResult + const defaultVariants: Variants = { + hidden: { + [direction === 'left' || direction === 'right' ? 'x' : 'y']: + direction === 'right' || direction === 'down' ? -offset : offset, + opacity: 0, + filter: `blur(${blur})` + }, + visible: { + [direction === 'left' || direction === 'right' ? 'x' : 'y']: 0, + opacity: 1, + filter: `blur(0px)` + } + } + const combinedVariants = variant || defaultVariants + return ( + + + {children} + + + ) +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 11e2bf70de..885f343a91 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -18,8 +18,8 @@ "paths": { "@main/*": ["src/main/*"], "@types": ["src/renderer/src/types/index.ts"], - "@components/*": ["packages/components/*"], - "@shared/*": ["packages/shared/*"] + "@shared/*": ["packages/shared/*"], + "@renderer/*": ["src/renderer/src/*"] } } } diff --git a/yarn.lock b/yarn.lock index f9e0d334fe..abd5141749 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5311,6 +5311,7 @@ __metadata: lucide-react: "npm:^0.509.0" markdown-it: "npm:^14.1.0" mime: "npm:^4.0.4" + motion: "npm:^12.11.0" next-themes: "npm:^0.4.6" node-stream-zip: "npm:^1.15.0" npx-scope-finder: "npm:^1.2.0" @@ -9435,6 +9436,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.11.0": + version: 12.11.0 + resolution: "framer-motion@npm:12.11.0" + dependencies: + motion-dom: "npm:^12.11.0" + motion-utils: "npm:^12.9.4" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/369026997d12e51ba5ca12ecf507ccf2108941c9e634d0d57ff93524d56d72dc38cd909ce1272a155d15b92faf0ddf9268942908be29032e36cc535a353fd497 + languageName: node + linkType: hard + "fresh@npm:^2.0.0": version: 2.0.0 resolution: "fresh@npm:2.0.0" @@ -13409,6 +13432,43 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.11.0": + version: 12.11.0 + resolution: "motion-dom@npm:12.11.0" + dependencies: + motion-utils: "npm:^12.9.4" + checksum: 10c0/9fd7441c38b28560ea2db0f4dbd6f412873e777f5d32e623792cc8ff32c0bbff761f68102115060af81325227cc639e548f6123bdced50722f55bc2abda76b55 + languageName: node + linkType: hard + +"motion-utils@npm:^12.9.4": + version: 12.9.4 + resolution: "motion-utils@npm:12.9.4" + checksum: 10c0/b6783babfd1282ad320585f7cdac9fe7a1f97b39e07d12a500d3709534441bd9d49b556fa1cd838d1bde188570d4ab6b4c5aa9d297f7f5aa9dc16d600c17afdc + languageName: node + linkType: hard + +"motion@npm:^12.11.0": + version: 12.11.0 + resolution: "motion@npm:12.11.0" + dependencies: + framer-motion: "npm:^12.11.0" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/1066de76a5d8a7a6898d8b47afe4363098466c67ed79cb989c0f05c11aa62a2cc83f61cc283ec07b452ae0622f6cd1c4584f1a0a68174798d67ba8811c52ac11 + languageName: node + linkType: hard + "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4"