Skip to content

Commit 78b5a16

Browse files
committed
polish
1 parent 0289efa commit 78b5a16

File tree

11 files changed

+956
-1052
lines changed

11 files changed

+956
-1052
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
'use client';
2+
3+
import {
4+
ArrowClockwiseIcon,
5+
CheckIcon,
6+
PlusIcon,
7+
TrashIcon,
8+
} from '@phosphor-icons/react';
9+
import { Badge } from '@/components/ui/badge';
10+
import { Button } from '@/components/ui/button';
11+
import { Card, CardContent } from '@/components/ui/card';
12+
13+
interface Extension {
14+
name: string;
15+
description: string;
16+
version?: string;
17+
defaultVersion?: string;
18+
schema?: string;
19+
hasStatefulData?: boolean;
20+
requiresRestart?: boolean;
21+
needsUpdate?: boolean;
22+
}
23+
24+
interface ExtensionCardProps {
25+
extension: Extension;
26+
type: 'installed' | 'available';
27+
onInstall?: () => void;
28+
onUpdate?: () => void;
29+
onRemove?: () => void;
30+
onReset?: () => void;
31+
canManage: boolean;
32+
isInstalling?: boolean;
33+
isUpdating?: boolean;
34+
isRemoving?: boolean;
35+
isResetting?: boolean;
36+
}
37+
38+
function ExtensionBadges({
39+
extension,
40+
type,
41+
}: {
42+
extension: Extension;
43+
type: 'installed' | 'available';
44+
}) {
45+
return (
46+
<div className="flex flex-wrap gap-1">
47+
{type === 'installed' ? (
48+
<Badge className="bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400">
49+
<CheckIcon className="mr-1 h-3 w-3" />
50+
Installed
51+
</Badge>
52+
) : (
53+
<Badge variant="outline">
54+
<PlusIcon className="mr-1 h-3 w-3" />
55+
Available
56+
</Badge>
57+
)}
58+
{extension.needsUpdate && (
59+
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-900/20 dark:text-amber-400">
60+
<ArrowClockwiseIcon className="mr-1 h-3 w-3" />
61+
Update Available
62+
</Badge>
63+
)}
64+
</div>
65+
);
66+
}
67+
68+
function ExtensionMetadata({
69+
extension,
70+
type,
71+
}: {
72+
extension: Extension;
73+
type: 'installed' | 'available';
74+
}) {
75+
return (
76+
<div className="flex flex-wrap gap-2">
77+
{type === 'installed' && extension.version && (
78+
<Badge className="text-xs" variant="secondary">
79+
v{extension.version}
80+
</Badge>
81+
)}
82+
{type === 'available' && extension.defaultVersion && (
83+
<Badge className="text-xs" variant="secondary">
84+
v{extension.defaultVersion}
85+
</Badge>
86+
)}
87+
{extension.schema && (
88+
<Badge className="text-xs" variant="outline">
89+
{extension.schema}
90+
</Badge>
91+
)}
92+
{extension.hasStatefulData && (
93+
<Badge className="text-xs" variant="outline">
94+
Stateful
95+
</Badge>
96+
)}
97+
{extension.requiresRestart && (
98+
<Badge
99+
className="border-amber-200 text-amber-700 text-xs dark:border-amber-800 dark:text-amber-300"
100+
variant="outline"
101+
>
102+
Restart Required
103+
</Badge>
104+
)}
105+
</div>
106+
);
107+
}
108+
109+
export function ExtensionCard({
110+
extension,
111+
type,
112+
onInstall,
113+
onUpdate,
114+
onRemove,
115+
onReset,
116+
canManage,
117+
isInstalling,
118+
isUpdating,
119+
isRemoving,
120+
isResetting,
121+
}: ExtensionCardProps) {
122+
const isLoading = isInstalling || isUpdating || isRemoving || isResetting;
123+
124+
return (
125+
<Card className="group relative overflow-hidden rounded border transition-all duration-200 hover:border-primary/20 hover:shadow-lg">
126+
<CardContent className="p-6">
127+
<div className="space-y-4">
128+
{/* Header */}
129+
<div className="flex items-start justify-between">
130+
<div className="min-w-0 flex-1">
131+
<h3 className="truncate font-semibold text-lg leading-tight">
132+
{extension.name}
133+
</h3>
134+
<p className="mt-1 line-clamp-2 text-muted-foreground text-sm">
135+
{extension.description}
136+
</p>
137+
</div>
138+
<div className="ml-3 flex-shrink-0">
139+
<ExtensionBadges extension={extension} type={type} />
140+
</div>
141+
</div>
142+
143+
{/* Metadata */}
144+
<ExtensionMetadata extension={extension} type={type} />
145+
146+
{/* Actions */}
147+
<div className="flex items-center justify-between pt-2">
148+
<div className="flex gap-2">
149+
{type === 'installed' && extension.needsUpdate && onUpdate && (
150+
<Button
151+
disabled={!canManage || isLoading}
152+
onClick={onUpdate}
153+
size="sm"
154+
variant="outline"
155+
>
156+
<ArrowClockwiseIcon className="mr-1 h-3 w-3" />
157+
{isUpdating ? 'Updating...' : 'Update'}
158+
</Button>
159+
)}
160+
{type === 'installed' && extension.hasStatefulData && onReset && (
161+
<Button
162+
disabled={!canManage || isLoading}
163+
onClick={onReset}
164+
size="sm"
165+
variant="outline"
166+
>
167+
{isResetting ? 'Resetting...' : 'Reset Stats'}
168+
</Button>
169+
)}
170+
</div>
171+
172+
<div className="flex gap-2">
173+
{type === 'available' && onInstall && (
174+
<Button
175+
className="bg-gradient-to-r from-primary to-primary/90 hover:from-primary/90 hover:to-primary"
176+
disabled={!canManage || isLoading}
177+
onClick={onInstall}
178+
size="sm"
179+
>
180+
<PlusIcon className="mr-1 h-3 w-3" />
181+
{isInstalling ? 'Installing...' : 'Install'}
182+
</Button>
183+
)}
184+
{type === 'installed' && onRemove && (
185+
<Button
186+
disabled={!canManage || isLoading}
187+
onClick={onRemove}
188+
size="sm"
189+
variant="destructive"
190+
>
191+
<TrashIcon className="mr-1 h-3 w-3" />
192+
{isRemoving ? 'Removing...' : 'Remove'}
193+
</Button>
194+
)}
195+
</div>
196+
</div>
197+
</div>
198+
</CardContent>
199+
200+
{/* Loading overlay */}
201+
{isLoading && (
202+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm">
203+
<div className="flex items-center gap-2 text-muted-foreground text-sm">
204+
<ArrowClockwiseIcon className="h-4 w-4 animate-spin" />
205+
Processing...
206+
</div>
207+
</div>
208+
)}
209+
</Card>
210+
);
211+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use client';
2+
3+
import {
4+
DatabaseIcon,
5+
MagnifyingGlassIcon,
6+
PlusIcon,
7+
} from '@phosphor-icons/react';
8+
import { Button } from '@/components/ui/button';
9+
import { Card, CardContent } from '@/components/ui/card';
10+
11+
interface ExtensionEmptyStateProps {
12+
type: 'installed' | 'available' | 'search';
13+
searchTerm?: string;
14+
onInstallExtension?: () => void;
15+
onClearSearch?: () => void;
16+
canManage?: boolean;
17+
}
18+
19+
export function ExtensionEmptyState({
20+
type,
21+
searchTerm,
22+
onInstallExtension,
23+
onClearSearch,
24+
canManage = false,
25+
}: ExtensionEmptyStateProps) {
26+
const getContent = () => {
27+
switch (type) {
28+
case 'installed':
29+
return {
30+
icon: (
31+
<DatabaseIcon
32+
className="h-12 w-12 text-muted-foreground"
33+
weight="duotone"
34+
/>
35+
),
36+
title: 'No Extensions Installed',
37+
description:
38+
'Get started by installing your first PostgreSQL extension to enhance your database capabilities.',
39+
action:
40+
canManage && onInstallExtension ? (
41+
<Button className="gap-2" onClick={onInstallExtension}>
42+
<PlusIcon className="h-4 w-4" />
43+
Install Extension
44+
</Button>
45+
) : null,
46+
};
47+
case 'available':
48+
return {
49+
icon: (
50+
<DatabaseIcon
51+
className="h-12 w-12 text-muted-foreground"
52+
weight="duotone"
53+
/>
54+
),
55+
title: 'No Available Extensions',
56+
description:
57+
'All available extensions have been installed or there are no extensions available for installation.',
58+
action: null,
59+
};
60+
case 'search':
61+
return {
62+
icon: (
63+
<MagnifyingGlassIcon
64+
className="h-12 w-12 text-muted-foreground"
65+
weight="duotone"
66+
/>
67+
),
68+
title: 'No Extensions Found',
69+
description: searchTerm
70+
? `No extensions match "${searchTerm}". Try adjusting your search terms.`
71+
: 'No extensions match your search criteria.',
72+
action: onClearSearch ? (
73+
<Button onClick={onClearSearch} variant="outline">
74+
Clear Search
75+
</Button>
76+
) : null,
77+
};
78+
default:
79+
return {
80+
icon: (
81+
<DatabaseIcon
82+
className="h-12 w-12 text-muted-foreground"
83+
weight="duotone"
84+
/>
85+
),
86+
title: 'No Extensions',
87+
description: 'No extensions available.',
88+
action: null,
89+
};
90+
}
91+
};
92+
93+
const content = getContent();
94+
95+
return (
96+
<Card className="rounded border-dashed">
97+
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
98+
<div className="mb-4 rounded-full border border-muted bg-muted/20 p-6">
99+
{content.icon}
100+
</div>
101+
<h3 className="mb-2 font-semibold text-lg">{content.title}</h3>
102+
<p className="mb-6 max-w-md text-muted-foreground text-sm">
103+
{content.description}
104+
</p>
105+
{content.action}
106+
</CardContent>
107+
</Card>
108+
);
109+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
3+
import { MagnifyingGlassIcon, XIcon } from '@phosphor-icons/react';
4+
import { Badge } from '@/components/ui/badge';
5+
import { Button } from '@/components/ui/button';
6+
import { Input } from '@/components/ui/input';
7+
8+
interface ExtensionSearchProps {
9+
search: string;
10+
onSearchChange: (search: string) => void;
11+
placeholder?: string;
12+
className?: string;
13+
}
14+
15+
export function ExtensionSearch({
16+
search,
17+
onSearchChange,
18+
placeholder = 'Search extensions...',
19+
className = '',
20+
}: ExtensionSearchProps) {
21+
const handleClear = () => {
22+
onSearchChange('');
23+
};
24+
25+
return (
26+
<div className={`relative max-w-md flex-1 ${className}`}>
27+
<MagnifyingGlassIcon className="-translate-y-1/2 absolute top-1/2 left-3 h-4 w-4 text-muted-foreground" />
28+
<Input
29+
className="rounded border-border/50 bg-background/50 pr-10 pl-10 backdrop-blur-sm transition-all duration-200 focus:border-primary/50 focus:bg-background"
30+
onChange={(e) => onSearchChange(e.target.value)}
31+
placeholder={placeholder}
32+
value={search}
33+
/>
34+
{search && (
35+
<Button
36+
className="-translate-y-1/2 absolute top-1/2 right-1 h-7 w-7 rounded-full"
37+
onClick={handleClear}
38+
size="sm"
39+
variant="ghost"
40+
>
41+
<XIcon className="h-3 w-3" />
42+
</Button>
43+
)}
44+
{search && (
45+
<div className="absolute top-full left-0 z-10 mt-2">
46+
<Badge className="text-xs" variant="secondary">
47+
Searching: {search}
48+
</Badge>
49+
</div>
50+
)}
51+
</div>
52+
);
53+
}

0 commit comments

Comments
 (0)