Compare commits
4 Commits
docs/draft
...
plugin-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecd934543e | ||
|
|
a4dd0560b9 | ||
|
|
15bab6d31e | ||
|
|
c22e8535dc |
@@ -91,6 +91,7 @@
|
||||
"graphql-http": "^1.22.0",
|
||||
"graphql-playground-html": "1.6.30",
|
||||
"http-status": "1.6.2",
|
||||
"lucide-react": "^0.460.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"qs-esm": "7.0.2",
|
||||
"react-diff-viewer-continued": "3.2.6",
|
||||
|
||||
@@ -27,6 +27,9 @@ export const DefaultNavClient: React.FC<{
|
||||
const { i18n } = useTranslation()
|
||||
const { navOpen } = useNav()
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
const LinkElement = Link || 'a'
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
@@ -46,10 +49,6 @@ export const DefaultNavClient: React.FC<{
|
||||
id = `nav-global-${slug}`
|
||||
}
|
||||
|
||||
const Link = (LinkWithDefault.default ||
|
||||
LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
const LinkElement = Link || 'a'
|
||||
const activeCollection =
|
||||
pathname.startsWith(href) && ['/', undefined].includes(pathname[href.length])
|
||||
|
||||
@@ -72,6 +71,11 @@ export const DefaultNavClient: React.FC<{
|
||||
</NavGroup>
|
||||
)
|
||||
})}
|
||||
<div className="nav-group Plugins" id="nav-group-Plugins">
|
||||
<LinkElement href={`/admin/plugins`}>
|
||||
<div className="nav-group__label">Plugins</div>
|
||||
</LinkElement>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
58
packages/next/src/views/Plugins/components/PackageCard.tsx
Normal file
58
packages/next/src/views/Plugins/components/PackageCard.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Download } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import type { Package } from './usePackages.js'
|
||||
|
||||
import { Rating } from './Rating.js'
|
||||
|
||||
interface PackageCardProps {
|
||||
pkg: Package
|
||||
view: 'grid' | 'list'
|
||||
}
|
||||
|
||||
export const PackageCard: React.FC<PackageCardProps> = ({ pkg, view }) => {
|
||||
const formatDownloads = (num: number) => {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const cardClassName = `package-card package-card--${view}`
|
||||
|
||||
return (
|
||||
<div className={cardClassName}>
|
||||
<div className="package-card__content">
|
||||
<div className="package-card__info">
|
||||
<h3 className="package-card__title">{pkg.name}</h3>
|
||||
<p
|
||||
className={`package-card__description ${
|
||||
view === 'grid' ? 'package-card__description--grid' : ''
|
||||
}`}
|
||||
>
|
||||
{pkg.description}
|
||||
</p>
|
||||
<div className="package-card__meta">
|
||||
<Rating value={pkg.rating} />
|
||||
<div className="package-card__downloads">
|
||||
<Download className="package-card__downloads-icon" />
|
||||
{formatDownloads(pkg.downloads)}
|
||||
</div>
|
||||
<span>v{pkg.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={`package-card__button ${
|
||||
pkg.isInstalled ? 'package-card__button--installed' : ''
|
||||
}`}
|
||||
type="button"
|
||||
>
|
||||
{pkg.isInstalled ? 'Installed' : 'Install'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
packages/next/src/views/Plugins/components/Rating.tsx
Normal file
21
packages/next/src/views/Plugins/components/Rating.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Star } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
interface RatingProps {
|
||||
value: number
|
||||
}
|
||||
|
||||
export const Rating: React.FC<RatingProps> = ({ value }) => {
|
||||
return (
|
||||
<div className="rating">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
className={`rating__star ${
|
||||
star <= value ? 'rating__star--filled' : 'rating__star--empty'
|
||||
}`}
|
||||
key={star}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
packages/next/src/views/Plugins/components/SearchBar.tsx
Normal file
22
packages/next/src/views/Plugins/components/SearchBar.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Search } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
interface SearchBarProps {
|
||||
onChange: (value: string) => void
|
||||
value: string
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({ onChange, value }) => {
|
||||
return (
|
||||
<div className="search-bar">
|
||||
<Search className="search-bar__icon" />
|
||||
<input
|
||||
className="search-bar__input"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Search extensions..."
|
||||
type="text"
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
packages/next/src/views/Plugins/components/TabSwitcher.tsx
Normal file
35
packages/next/src/views/Plugins/components/TabSwitcher.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { TabMode } from '../types'
|
||||
|
||||
interface TabSwitcherProps {
|
||||
activeTab: TabMode
|
||||
installedCount: number
|
||||
onTabChange: (tab: TabMode) => void
|
||||
}
|
||||
|
||||
export const TabSwitcher: React.FC<TabSwitcherProps> = ({
|
||||
activeTab,
|
||||
installedCount,
|
||||
onTabChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="tab-switcher">
|
||||
<button
|
||||
className={`tab-switcher__tab ${activeTab === 'all' ? 'tab-switcher__tab--active' : ''}`}
|
||||
onClick={() => onTabChange('all')}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
className={`tab-switcher__tab ${
|
||||
activeTab === 'installed' ? 'tab-switcher__tab--active' : ''
|
||||
}`}
|
||||
onClick={() => onTabChange('installed')}
|
||||
>
|
||||
Installed
|
||||
<span className="tab-switcher__count">{installedCount}</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
packages/next/src/views/Plugins/components/ViewToggle.tsx
Normal file
28
packages/next/src/views/Plugins/components/ViewToggle.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LayoutGrid, List } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import type { ViewMode } from '../types'
|
||||
|
||||
interface ViewToggleProps {
|
||||
onViewChange: (view: ViewMode) => void
|
||||
view: ViewMode
|
||||
}
|
||||
|
||||
export const ViewToggle: React.FC<ViewToggleProps> = ({ onViewChange, view }) => {
|
||||
return (
|
||||
<div className="view-toggle">
|
||||
<button
|
||||
className={`view-toggle__button ${view === 'list' ? 'view-toggle__button--active' : ''}`}
|
||||
onClick={() => onViewChange('list')}
|
||||
>
|
||||
<List className="view-toggle__icon" />
|
||||
</button>
|
||||
<button
|
||||
className={`view-toggle__button ${view === 'grid' ? 'view-toggle__button--active' : ''}`}
|
||||
onClick={() => onViewChange('grid')}
|
||||
>
|
||||
<LayoutGrid className="view-toggle__icon" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
packages/next/src/views/Plugins/components/usePackages.tsx
Normal file
94
packages/next/src/views/Plugins/components/usePackages.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export interface Package {
|
||||
description: string
|
||||
downloads: number
|
||||
isInstalled?: boolean
|
||||
name: string
|
||||
rating: number
|
||||
version: string
|
||||
}
|
||||
|
||||
// Mocked data for demonstration
|
||||
const MOCK_PACKAGES: Package[] = [
|
||||
{
|
||||
name: '@payloadcms/plugin-cloud',
|
||||
description: 'Official Payload Cloud plugin for PayloadCMS',
|
||||
downloads: 25000,
|
||||
isInstalled: true,
|
||||
rating: 4.8,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: '@payloadcms/plugin-seo',
|
||||
description: 'SEO plugin for PayloadCMS with automatic meta tags and sitemap generation',
|
||||
downloads: 45000,
|
||||
isInstalled: true,
|
||||
rating: 4.9,
|
||||
version: '2.1.0',
|
||||
},
|
||||
{
|
||||
name: '@payloadcms/plugin-nested-docs',
|
||||
description: 'Nested documents plugin for PayloadCMS',
|
||||
downloads: 12000,
|
||||
rating: 4.5,
|
||||
version: '0.9.0',
|
||||
},
|
||||
{
|
||||
name: '@payloadcms/plugin-form-builder',
|
||||
description: 'Powerful form builder plugin for PayloadCMS',
|
||||
downloads: 32000,
|
||||
rating: 4.7,
|
||||
version: '1.2.0',
|
||||
},
|
||||
{
|
||||
name: 'payload-plugin-custom-fields',
|
||||
description: 'Custom fields collection for PayloadCMS',
|
||||
downloads: 8000,
|
||||
rating: 4.2,
|
||||
version: '0.5.0',
|
||||
},
|
||||
{
|
||||
name: '@payloadcms/plugin-redirects',
|
||||
description: 'URL redirects management for PayloadCMS',
|
||||
downloads: 15000,
|
||||
rating: 4.6,
|
||||
version: '1.1.0',
|
||||
},
|
||||
]
|
||||
|
||||
export const usePackages = (searchTerm: string, activeTab: 'all' | 'installed') => {
|
||||
const [packages, setPackages] = useState<Package[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate API call
|
||||
const fetchPackages = async () => {
|
||||
setLoading(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
|
||||
let filteredPackages = MOCK_PACKAGES
|
||||
|
||||
if (searchTerm) {
|
||||
filteredPackages = filteredPackages.filter(
|
||||
(pkg) =>
|
||||
pkg.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
pkg.description.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
}
|
||||
|
||||
if (activeTab === 'installed') {
|
||||
filteredPackages = filteredPackages.filter((pkg) => pkg.isInstalled)
|
||||
}
|
||||
|
||||
setPackages(filteredPackages)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchPackages().catch((error) => {
|
||||
throw new Error(error)
|
||||
})
|
||||
}, [searchTerm, activeTab])
|
||||
|
||||
return { loading, packages }
|
||||
}
|
||||
72
packages/next/src/views/Plugins/index.tsx
Normal file
72
packages/next/src/views/Plugins/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import { PackageSearch } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import type { Package } from './components/usePackages.js'
|
||||
|
||||
import { PackageCard } from './components/PackageCard.js'
|
||||
import { SearchBar } from './components/SearchBar.js'
|
||||
import { TabSwitcher } from './components/TabSwitcher.js'
|
||||
import { usePackages } from './components/usePackages.js'
|
||||
import { ViewToggle } from './components/ViewToggle.js'
|
||||
import './styles/main.scss'
|
||||
|
||||
type ViewMode = 'grid' | 'list'
|
||||
type TabMode = 'all' | 'installed'
|
||||
|
||||
export const PluginsView: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [view, setView] = useState<ViewMode>('list')
|
||||
const [activeTab, setActiveTab] = useState<TabMode>('all')
|
||||
|
||||
const { loading, packages } = usePackages(searchTerm, activeTab)
|
||||
const installedCount = packages.filter((pkg) => pkg.isInstalled).length
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="app">
|
||||
<div className="container">
|
||||
<header className="app__header">
|
||||
<div className="app__header-title">
|
||||
<PackageSearch className="app__header-title-icon" />
|
||||
<h1 className="app__header-title-text">PayloadCMS Plugins</h1>
|
||||
</div>
|
||||
<ViewToggle onViewChange={setView} view={view} />
|
||||
</header>
|
||||
|
||||
<div className="app__controls">
|
||||
<SearchBar onChange={setSearchTerm} value={searchTerm} />
|
||||
<TabSwitcher
|
||||
activeTab={activeTab}
|
||||
installedCount={installedCount}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div className="loading__item" key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : packages.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<PackageSearch className="empty-state__icon" />
|
||||
<h3 className="empty-state__title">No packages found</h3>
|
||||
<p className="empty-state__description">
|
||||
Try adjusting your search or filters to find what you're looking for
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`app__content app__content--${view}`}>
|
||||
{packages.map((pkg: Package) => (
|
||||
<PackageCard key={pkg.name} pkg={pkg} view={view} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
60
packages/next/src/views/Plugins/styles/_mixins.scss
Normal file
60
packages/next/src/views/Plugins/styles/_mixins.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $gap: 0) {
|
||||
display: flex;
|
||||
flex-direction: $direction;
|
||||
justify-content: $justify;
|
||||
align-items: $align;
|
||||
@if $gap != 0 {
|
||||
gap: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-base {
|
||||
padding: $spacing-2 $spacing-4;
|
||||
border-radius: $radius-full;
|
||||
transition: $transition-all;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba($color-primary, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-primary {
|
||||
@include button-base;
|
||||
background-color: $color-primary;
|
||||
color: $color-white;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-primary-hover;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-secondary {
|
||||
@include button-base;
|
||||
background-color: $color-gray-100;
|
||||
color: $color-gray-700;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-gray-200;
|
||||
color: $color-gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin card-base {
|
||||
background-color: $color-white;
|
||||
border-radius: $radius-xl;
|
||||
border: 1px solid $color-gray-200;
|
||||
overflow: hidden;
|
||||
}
|
||||
46
packages/next/src/views/Plugins/styles/_variables.scss
Normal file
46
packages/next/src/views/Plugins/styles/_variables.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
// Colors
|
||||
$color-primary: #000000;
|
||||
$color-primary-hover: #272727;
|
||||
$color-primary-light: rgba(0, 64, 255, 0.1);
|
||||
$color-gray-50: #f8fafc;
|
||||
$color-gray-100: #f1f5f9;
|
||||
$color-gray-200: #e2e8f0;
|
||||
$color-gray-300: #cbd5e1;
|
||||
$color-gray-400: #94a3b8;
|
||||
$color-gray-500: #64748b;
|
||||
$color-gray-600: #475569;
|
||||
$color-gray-700: #334155;
|
||||
$color-gray-800: #1e293b;
|
||||
$color-gray-900: #0f172a;
|
||||
$color-white: #ffffff;
|
||||
$color-yellow-400: #fbbf24;
|
||||
|
||||
// Shadows
|
||||
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
// Spacing
|
||||
$spacing-1: 0.25rem;
|
||||
$spacing-2: 0.5rem;
|
||||
$spacing-3: 0.75rem;
|
||||
$spacing-4: 1rem;
|
||||
$spacing-5: 1.25rem;
|
||||
$spacing-6: 1.5rem;
|
||||
$spacing-8: 2rem;
|
||||
$spacing-12: 3rem;
|
||||
|
||||
// Border radius
|
||||
$radius-md: 0.375rem;
|
||||
$radius-lg: 0.5rem;
|
||||
$radius-xl: 0.75rem;
|
||||
$radius-full: 9999px;
|
||||
|
||||
// Transitions
|
||||
$transition-all: all 0.2s ease-in-out;
|
||||
|
||||
// Breakpoints
|
||||
$breakpoint-sm: 640px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 1024px;
|
||||
$breakpoint-xl: 1280px;
|
||||
@@ -0,0 +1,25 @@
|
||||
@import '../variables';
|
||||
@import '../mixins';
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: $spacing-12 0;
|
||||
|
||||
&__icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: $color-gray-400;
|
||||
margin: 0 auto $spacing-4;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
color: $color-gray-900;
|
||||
margin-bottom: $spacing-2;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: $color-gray-600;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@import '../variables';
|
||||
|
||||
.loading {
|
||||
display: grid;
|
||||
gap: $spacing-4;
|
||||
|
||||
&__item {
|
||||
@include card-base;
|
||||
height: 8rem;
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
@import '../variables';
|
||||
@import '../mixins';
|
||||
|
||||
.package-card {
|
||||
@include card-base;
|
||||
background-color: $color-white;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: $transition-all;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
&--list {
|
||||
padding: $spacing-5;
|
||||
|
||||
.package-card__content {
|
||||
@include flex($justify: space-between, $align: center, $gap: $spacing-6);
|
||||
}
|
||||
|
||||
.package-card__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.package-card__button {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
&--grid {
|
||||
padding: $spacing-6;
|
||||
height: 100%;
|
||||
|
||||
.package-card__content {
|
||||
@include flex($direction: column, $justify: space-between);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.package-card__button {
|
||||
margin-top: $spacing-4;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: $color-gray-900;
|
||||
margin-bottom: $spacing-2;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: $color-gray-600;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: $spacing-4;
|
||||
|
||||
&--grid {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
@include flex($align: center, $gap: $spacing-4);
|
||||
color: $color-gray-500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__downloads {
|
||||
@include flex($align: center, $gap: $spacing-1);
|
||||
color: $color-gray-600;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__button {
|
||||
@include button-primary;
|
||||
font-weight: 500;
|
||||
height: 2.5rem;
|
||||
|
||||
&--installed {
|
||||
background-color: $color-primary-light;
|
||||
color: $color-primary;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($color-primary-light, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@import '../variables';
|
||||
|
||||
.rating {
|
||||
@include flex($align: center, $gap: $spacing-1);
|
||||
|
||||
&__star {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
||||
&--filled {
|
||||
color: $color-yellow-400;
|
||||
fill: $color-yellow-400;
|
||||
}
|
||||
|
||||
&--empty {
|
||||
color: $color-gray-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@import '../variables';
|
||||
@import '../mixins';
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 36rem;
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
left: $spacing-4;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: $color-gray-400;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
padding: $spacing-3 $spacing-4 $spacing-3 $spacing-12;
|
||||
border-radius: $radius-full;
|
||||
background-color: $color-white;
|
||||
border: 1px solid $color-gray-200;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: $transition-all;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:focus {
|
||||
border-color: $color-primary;
|
||||
box-shadow: 0 0 0 3px rgba($color-primary, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $color-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@import '../variables';
|
||||
@import '../mixins';
|
||||
|
||||
.tab-switcher {
|
||||
@include flex($justify: center, $align: center, $gap: $spacing-1);
|
||||
background-color: $color-gray-100;
|
||||
padding: $spacing-1;
|
||||
border-radius: $radius-full;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&__tab {
|
||||
@include button-base;
|
||||
padding: $spacing-2 $spacing-4;
|
||||
border-radius: $radius-full;
|
||||
font-weight: 500;
|
||||
|
||||
&--active {
|
||||
background-color: $color-white;
|
||||
color: $color-primary;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
&:not(&--active) {
|
||||
color: $color-gray-600;
|
||||
|
||||
&:hover {
|
||||
color: $color-gray-900;
|
||||
background-color: rgba($color-gray-200, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
margin-left: $spacing-2;
|
||||
background-color: $color-primary-light;
|
||||
color: $color-primary;
|
||||
padding: 0.125rem $spacing-2;
|
||||
font-size: 0.75rem;
|
||||
border-radius: $radius-full;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@import '../variables';
|
||||
@import '../mixins';
|
||||
|
||||
.view-toggle {
|
||||
@include flex($justify: center, $align: center);
|
||||
background-color: $color-gray-100;
|
||||
padding: $spacing-1;
|
||||
border-radius: $radius-full;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&__button {
|
||||
@include button-base;
|
||||
padding: $spacing-2 $spacing-3;
|
||||
border-radius: $radius-full;
|
||||
|
||||
&--active {
|
||||
background-color: $color-white;
|
||||
color: $color-primary;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
&:not(&--active) {
|
||||
color: $color-gray-600;
|
||||
|
||||
&:hover {
|
||||
color: $color-gray-900;
|
||||
background-color: rgba($color-gray-200, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
88
packages/next/src/views/Plugins/styles/main.scss
Normal file
88
packages/next/src/views/Plugins/styles/main.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
// Layout
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 $spacing-4;
|
||||
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
padding: 0 $spacing-6;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-lg) {
|
||||
padding: 0 $spacing-8;
|
||||
}
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
padding-bottom: $spacing-12;
|
||||
|
||||
&__header {
|
||||
@include flex($justify: space-between, $align: center);
|
||||
margin-bottom: $spacing-8;
|
||||
padding-top: $spacing-8;
|
||||
|
||||
&-title {
|
||||
@include flex($align: center, $gap: $spacing-3);
|
||||
|
||||
&-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, $color-primary 0%, darken($color-primary, 15%) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
@include flex($direction: column, $gap: $spacing-4);
|
||||
margin-bottom: $spacing-8;
|
||||
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
&--grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: $spacing-6;
|
||||
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-lg) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
&--list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import component styles
|
||||
@import 'components/search-bar';
|
||||
@import 'components/view-toggle';
|
||||
@import 'components/tab-switcher';
|
||||
@import 'components/rating';
|
||||
@import 'components/package-card';
|
||||
@import 'components/empty-state';
|
||||
@import 'components/loading';
|
||||
@@ -20,6 +20,7 @@ import { forgotPasswordBaseClass, ForgotPasswordView } from '../ForgotPassword/i
|
||||
import { ListView } from '../List/index.js'
|
||||
import { loginBaseClass, LoginView } from '../Login/index.js'
|
||||
import { LogoutInactivity, LogoutView } from '../Logout/index.js'
|
||||
import { PluginsView } from '../Plugins/index.js'
|
||||
import { ResetPassword, resetPasswordBaseClass } from '../ResetPassword/index.js'
|
||||
import { UnauthorizedView } from '../Unauthorized/index.js'
|
||||
import { Verify, verifyBaseClass } from '../Verify/index.js'
|
||||
@@ -30,6 +31,7 @@ const baseClasses = {
|
||||
account: 'account',
|
||||
forgot: forgotPasswordBaseClass,
|
||||
login: loginBaseClass,
|
||||
plugins: 'plugins',
|
||||
reset: resetPasswordBaseClass,
|
||||
verify: verifyBaseClass,
|
||||
}
|
||||
@@ -50,6 +52,7 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
inactivity: LogoutInactivity,
|
||||
login: LoginView,
|
||||
logout: LogoutView,
|
||||
plugins: PluginsView,
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
@@ -165,6 +168,7 @@ export const getViewFromConfig = ({
|
||||
// --> /logout
|
||||
// --> /logout-inactivity
|
||||
// --> /unauthorized
|
||||
// --> /plugins
|
||||
|
||||
ViewToRender = {
|
||||
Component: oneSegmentViews[viewKey],
|
||||
@@ -173,7 +177,7 @@ export const getViewFromConfig = ({
|
||||
templateClassName = baseClasses[viewKey]
|
||||
templateType = 'minimal'
|
||||
|
||||
if (viewKey === 'account') {
|
||||
if (viewKey === 'account' || viewKey === 'plugins') {
|
||||
templateType = 'default'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
inactivity: '/logout-inactivity',
|
||||
login: '/login',
|
||||
logout: '/logout',
|
||||
plugins: '/plugins',
|
||||
reset: '/reset',
|
||||
unauthorized: '/unauthorized',
|
||||
},
|
||||
|
||||
@@ -826,6 +826,8 @@ export type Config = {
|
||||
login?: string
|
||||
/** The route for the logout page. */
|
||||
logout?: string
|
||||
/** The route for the plugins page. */
|
||||
plugins?: string
|
||||
/** The route for the reset password page. */
|
||||
reset?: string
|
||||
/** The route for the unauthorized page. */
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -718,6 +718,9 @@ importers:
|
||||
http-status:
|
||||
specifier: 1.6.2
|
||||
version: 1.6.2
|
||||
lucide-react:
|
||||
specifier: ^0.460.0
|
||||
version: 0.460.0(react@19.0.0-rc-65a56d0e-20241020)
|
||||
next:
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@0.0.0-experimental-24ec0eb-20240918)(react-dom@19.0.0-rc-65a56d0e-20241020(react@19.0.0-rc-65a56d0e-20241020))(react@19.0.0-rc-65a56d0e-20241020)(sass@1.77.4)
|
||||
@@ -7615,6 +7618,11 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.460.0:
|
||||
resolution: {integrity: sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==}
|
||||
peerDependencies:
|
||||
react: 19.0.0-rc-65a56d0e-20241020
|
||||
|
||||
magic-string@0.30.12:
|
||||
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
|
||||
|
||||
@@ -17338,6 +17346,10 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.460.0(react@19.0.0-rc-65a56d0e-20241020):
|
||||
dependencies:
|
||||
react: 19.0.0-rc-65a56d0e-20241020
|
||||
|
||||
magic-string@0.30.12:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
Reference in New Issue
Block a user