feat: better component abstractions
This commit is contained in:
parent
962f1d798f
commit
455a55448a
11
apps/client/src/components/demo/Avatar.tsx
Normal file
11
apps/client/src/components/demo/Avatar.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
interface AvatarProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function Avatar({ name }: AvatarProps) {
|
||||
return (
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center text-white text-sm font-medium">
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
apps/client/src/components/demo/CodePreview.tsx
Normal file
14
apps/client/src/components/demo/CodePreview.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
interface CodePreviewProps {
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export function CodePreview({ data }: CodePreviewProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5 rounded-lg" />
|
||||
<pre className="relative p-4 rounded-lg text-sm font-mono overflow-x-auto">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
apps/client/src/components/demo/DemoCard.tsx
Normal file
43
apps/client/src/components/demo/DemoCard.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface DemoCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: LucideIcon;
|
||||
iconColorClass?: string;
|
||||
glowClass?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DemoCard({
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
iconColorClass = "bg-primary/10 text-primary",
|
||||
glowClass = "glow-primary",
|
||||
children,
|
||||
}: DemoCardProps) {
|
||||
return (
|
||||
<Card className="card-hover glass-card overflow-hidden">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${iconColorClass} ${glowClass}`}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
7
apps/client/src/components/demo/EmptyState.tsx
Normal file
7
apps/client/src/components/demo/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
interface EmptyStateProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function EmptyState({ message }: EmptyStateProps) {
|
||||
return <p className="text-center text-muted-foreground py-4">{message}</p>;
|
||||
}
|
||||
14
apps/client/src/components/demo/ItemRow.tsx
Normal file
14
apps/client/src/components/demo/ItemRow.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
interface ItemRowProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ItemRow({ children, className = "" }: ItemRowProps) {
|
||||
return (
|
||||
<div
|
||||
className={`item-row flex items-center gap-3 p-4 rounded-lg bg-muted/30 border border-border/50 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
apps/client/src/components/demo/LoadingSpinner.tsx
Normal file
31
apps/client/src/components/demo/LoadingSpinner.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
size?: "sm" | "md" | "lg";
|
||||
colorClass?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-6 w-6",
|
||||
lg: "h-8 w-8",
|
||||
};
|
||||
|
||||
export function LoadingSpinner({
|
||||
size = "lg",
|
||||
colorClass = "text-primary",
|
||||
className = "",
|
||||
}: LoadingSpinnerProps) {
|
||||
return (
|
||||
<div className={`flex justify-center py-8 ${className}`}>
|
||||
<Loader2
|
||||
className={`animate-spin pulse-glow ${sizeClasses[size]} ${colorClass}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InlineSpinner({ size = "sm" }: { size?: "sm" | "md" }) {
|
||||
return <Loader2 className={`animate-spin ${sizeClasses[size]}`} />;
|
||||
}
|
||||
20
apps/client/src/components/demo/StatusBadge.tsx
Normal file
20
apps/client/src/components/demo/StatusBadge.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
interface StatusBadgeProps {
|
||||
status: "success" | "warning" | "info";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const badgeClasses = {
|
||||
success: "badge-success",
|
||||
warning: "badge-warning",
|
||||
info: "bg-blue-500/10 text-blue-500",
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, children }: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full font-medium ${badgeClasses[status]}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
48
apps/client/src/components/demo/examples/HelloExample.tsx
Normal file
48
apps/client/src/components/demo/examples/HelloExample.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Sparkles, Zap } from "lucide-react";
|
||||
import { DemoCard, InlineSpinner, CodePreview } from "@/components/demo";
|
||||
|
||||
export function HelloExample() {
|
||||
const [name, setName] = useState("");
|
||||
const { data, isLoading, refetch } = trpc.hello.useQuery(
|
||||
{ name: name || undefined },
|
||||
{ enabled: false }
|
||||
);
|
||||
|
||||
return (
|
||||
<DemoCard
|
||||
title="Hello Query"
|
||||
description="Simple query to test the connection"
|
||||
icon={Zap}
|
||||
iconColorClass="bg-primary/10 text-primary"
|
||||
glowClass="glow-primary"
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter your name..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{isLoading ? (
|
||||
<InlineSpinner />
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Send
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{data && <CodePreview data={data} />}
|
||||
</DemoCard>
|
||||
);
|
||||
}
|
||||
209
apps/client/src/components/demo/examples/PostsExample.tsx
Normal file
209
apps/client/src/components/demo/examples/PostsExample.tsx
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { FileText, Plus, Sparkles, Trash2 } from "lucide-react";
|
||||
import {
|
||||
DemoCard,
|
||||
LoadingSpinner,
|
||||
InlineSpinner,
|
||||
EmptyState,
|
||||
StatusBadge,
|
||||
} from "@/components/demo";
|
||||
|
||||
interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string | null;
|
||||
published: boolean;
|
||||
}
|
||||
|
||||
export function PostsExample() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [authorId, setAuthorId] = useState("1");
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const { data: posts, isLoading } = trpc.posts.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.posts.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.posts.getAll.invalidate();
|
||||
setTitle("");
|
||||
setContent("");
|
||||
},
|
||||
});
|
||||
|
||||
const publishMutation = trpc.posts.publish.useMutation({
|
||||
onSuccess: () => utils.posts.getAll.invalidate(),
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.posts.delete.useMutation({
|
||||
onSuccess: () => utils.posts.getAll.invalidate(),
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
createMutation.mutate({
|
||||
title,
|
||||
content,
|
||||
authorId: parseInt(authorId),
|
||||
published: false,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DemoCard
|
||||
title="Posts"
|
||||
description="With publish action and filtering"
|
||||
icon={FileText}
|
||||
iconColorClass="bg-accent/10 text-accent"
|
||||
glowClass="glow-accent"
|
||||
>
|
||||
{/* Create Form */}
|
||||
<CreatePostForm
|
||||
title={title}
|
||||
content={content}
|
||||
authorId={authorId}
|
||||
onTitleChange={setTitle}
|
||||
onContentChange={setContent}
|
||||
onAuthorIdChange={setAuthorId}
|
||||
onSubmit={handleCreate}
|
||||
isLoading={createMutation.isPending}
|
||||
/>
|
||||
|
||||
{/* Posts List */}
|
||||
{isLoading ? (
|
||||
<LoadingSpinner colorClass="text-accent" />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{posts?.length === 0 && (
|
||||
<EmptyState message="No posts yet. Create your first post!" />
|
||||
)}
|
||||
{posts?.map((post) => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onPublish={() => publishMutation.mutate({ id: post.id })}
|
||||
onDelete={() => deleteMutation.mutate({ id: post.id })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DemoCard>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sub-components
|
||||
// ============================================================================
|
||||
|
||||
interface CreatePostFormProps {
|
||||
title: string;
|
||||
content: string;
|
||||
authorId: string;
|
||||
onTitleChange: (value: string) => void;
|
||||
onContentChange: (value: string) => void;
|
||||
onAuthorIdChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function CreatePostForm({
|
||||
title,
|
||||
content,
|
||||
authorId,
|
||||
onTitleChange,
|
||||
onContentChange,
|
||||
onAuthorIdChange,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
}: CreatePostFormProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => onTitleChange(e.target.value)}
|
||||
placeholder="Post title..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="w-28">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">
|
||||
Author ID
|
||||
</Label>
|
||||
<Input
|
||||
value={authorId}
|
||||
onChange={(e) => onAuthorIdChange(e.target.value)}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={content}
|
||||
onChange={(e) => onContentChange(e.target.value)}
|
||||
placeholder="Write something amazing..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={onSubmit} disabled={!title || !content || isLoading}>
|
||||
{isLoading ? (
|
||||
<InlineSpinner />
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PostCardProps {
|
||||
post: Post;
|
||||
onPublish: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function PostCard({ post, onPublish, onDelete }: PostCardProps) {
|
||||
return (
|
||||
<div className="item-row p-4 rounded-lg bg-muted/30 border border-border/50">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold truncate">{post.title}</h3>
|
||||
<StatusBadge status={post.published ? "success" : "warning"}>
|
||||
{post.published ? "Published" : "Draft"}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{post.content}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{!post.published && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-secondary border-secondary/30 hover:bg-secondary/10"
|
||||
onClick={onPublish}
|
||||
>
|
||||
<Sparkles className="h-3 w-3 mr-1" />
|
||||
Publish
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
241
apps/client/src/components/demo/examples/UsersExample.tsx
Normal file
241
apps/client/src/components/demo/examples/UsersExample.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Users, Plus, Edit2, Check, X, Trash2 } from "lucide-react";
|
||||
import {
|
||||
DemoCard,
|
||||
LoadingSpinner,
|
||||
InlineSpinner,
|
||||
EmptyState,
|
||||
ItemRow,
|
||||
Avatar,
|
||||
} from "@/components/demo";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function UsersExample() {
|
||||
const [newName, setNewName] = useState("");
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState("");
|
||||
const [editEmail, setEditEmail] = useState("");
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const { data: users, isLoading } = trpc.users.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.users.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.users.getAll.invalidate();
|
||||
setNewName("");
|
||||
setNewEmail("");
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = trpc.users.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.users.getAll.invalidate();
|
||||
setEditingId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.users.delete.useMutation({
|
||||
onSuccess: () => utils.users.getAll.invalidate(),
|
||||
});
|
||||
|
||||
const startEditing = (user: User) => {
|
||||
setEditingId(user.id);
|
||||
setEditName(user.name);
|
||||
setEditEmail(user.email);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
createMutation.mutate({ name: newName, email: newEmail });
|
||||
};
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
updateMutation.mutate({ id, name: editName, email: editEmail });
|
||||
};
|
||||
|
||||
return (
|
||||
<DemoCard
|
||||
title="Users"
|
||||
description="Full CRUD operations example"
|
||||
icon={Users}
|
||||
iconColorClass="bg-secondary/10 text-secondary"
|
||||
glowClass="glow-secondary"
|
||||
>
|
||||
{/* Create Form */}
|
||||
<CreateUserForm
|
||||
name={newName}
|
||||
email={newEmail}
|
||||
onNameChange={setNewName}
|
||||
onEmailChange={setNewEmail}
|
||||
onSubmit={handleCreate}
|
||||
isLoading={createMutation.isPending}
|
||||
/>
|
||||
|
||||
{/* Users List */}
|
||||
{isLoading ? (
|
||||
<LoadingSpinner colorClass="text-secondary" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{users?.length === 0 && (
|
||||
<EmptyState message="No users yet. Create one above!" />
|
||||
)}
|
||||
{users?.map((user) => (
|
||||
<UserRow
|
||||
key={user.id}
|
||||
user={user}
|
||||
isEditing={editingId === user.id}
|
||||
editName={editName}
|
||||
editEmail={editEmail}
|
||||
onEditNameChange={setEditName}
|
||||
onEditEmailChange={setEditEmail}
|
||||
onStartEdit={() => startEditing(user)}
|
||||
onCancelEdit={() => setEditingId(null)}
|
||||
onSaveEdit={() => handleUpdate(user.id)}
|
||||
onDelete={() => deleteMutation.mutate({ id: user.id })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DemoCard>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sub-components
|
||||
// ============================================================================
|
||||
|
||||
interface CreateUserFormProps {
|
||||
name: string;
|
||||
email: string;
|
||||
onNameChange: (value: string) => void;
|
||||
onEmailChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function CreateUserForm({
|
||||
name,
|
||||
email,
|
||||
onNameChange,
|
||||
onEmailChange,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
}: CreateUserFormProps) {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="Name"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => onEmailChange(e.target.value)}
|
||||
placeholder="Email"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={!name || !email || isLoading}
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
>
|
||||
{isLoading ? <InlineSpinner /> : <Plus className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserRowProps {
|
||||
user: User;
|
||||
isEditing: boolean;
|
||||
editName: string;
|
||||
editEmail: string;
|
||||
onEditNameChange: (value: string) => void;
|
||||
onEditEmailChange: (value: string) => void;
|
||||
onStartEdit: () => void;
|
||||
onCancelEdit: () => void;
|
||||
onSaveEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function UserRow({
|
||||
user,
|
||||
isEditing,
|
||||
editName,
|
||||
editEmail,
|
||||
onEditNameChange,
|
||||
onEditEmailChange,
|
||||
onStartEdit,
|
||||
onCancelEdit,
|
||||
onSaveEdit,
|
||||
onDelete,
|
||||
}: UserRowProps) {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<ItemRow>
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => onEditNameChange(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={editEmail}
|
||||
onChange={(e) => onEditEmailChange(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-green-500 hover:text-green-600 hover:bg-green-500/10"
|
||||
onClick={onSaveEdit}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</ItemRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemRow>
|
||||
<Avatar name={user.name} />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{user.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{user.email}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
onClick={onStartEdit}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</ItemRow>
|
||||
);
|
||||
}
|
||||
3
apps/client/src/components/demo/examples/index.ts
Normal file
3
apps/client/src/components/demo/examples/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { HelloExample } from "./HelloExample";
|
||||
export { UsersExample } from "./UsersExample";
|
||||
export { PostsExample } from "./PostsExample";
|
||||
7
apps/client/src/components/demo/index.ts
Normal file
7
apps/client/src/components/demo/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { DemoCard } from "./DemoCard";
|
||||
export { LoadingSpinner, InlineSpinner } from "./LoadingSpinner";
|
||||
export { EmptyState } from "./EmptyState";
|
||||
export { ItemRow } from "./ItemRow";
|
||||
export { Avatar } from "./Avatar";
|
||||
export { StatusBadge } from "./StatusBadge";
|
||||
export { CodePreview } from "./CodePreview";
|
||||
|
|
@ -362,7 +362,7 @@
|
|||
}
|
||||
|
||||
.card-hover:hover {
|
||||
box-shadow: 0 20px 40px -12px hsl(var(--primary) / 0.25);
|
||||
box-shadow: 0 10px 20px -12px hsl(var(--primary) / 0.25);
|
||||
}
|
||||
|
||||
/* Glassmorphism card */
|
||||
|
|
|
|||
|
|
@ -1,34 +1,16 @@
|
|||
import { useState } from "react";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import {
|
||||
Loader2,
|
||||
Plus,
|
||||
Trash2,
|
||||
Edit2,
|
||||
Check,
|
||||
X,
|
||||
Sparkles,
|
||||
Users,
|
||||
FileText,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
HelloExample,
|
||||
UsersExample,
|
||||
PostsExample,
|
||||
} from "@/components/demo/examples";
|
||||
|
||||
export default function TRPCDemoPage() {
|
||||
return (
|
||||
<div className="min-h-screen gradient-bg">
|
||||
<div className="container mx-auto p-8 max-w-4xl">
|
||||
{/* Hero Header */}
|
||||
<div className="mb-12 text-center">
|
||||
<header className="mb-12 text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary text-sm font-medium mb-4">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Type-safe API Demo
|
||||
|
|
@ -40,410 +22,20 @@ export default function TRPCDemoPage() {
|
|||
Experience the power of end-to-end type safety with a beautiful,
|
||||
modern interface
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-8">
|
||||
{/* Demo Examples */}
|
||||
<main className="grid gap-8">
|
||||
<HelloExample />
|
||||
<UsersExample />
|
||||
<PostsExample />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-12 text-center text-sm text-muted-foreground">
|
||||
<p>Built with 💜 using tRPC, React Query & TaylorDB</p>
|
||||
</div>
|
||||
<footer className="mt-12 text-center text-sm text-muted-foreground">
|
||||
<p>Built with 💜 using tRPC, React Query & TaylorDB</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hello Query Example
|
||||
// ============================================================================
|
||||
|
||||
function HelloExample() {
|
||||
const [name, setName] = useState("");
|
||||
const { data, isLoading, refetch } = trpc.hello.useQuery(
|
||||
{ name: name || undefined },
|
||||
{ enabled: false }
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="card-hover glass-card overflow-hidden">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10 glow-primary">
|
||||
<Zap className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">Hello Query</CardTitle>
|
||||
<CardDescription>
|
||||
Simple query to test the connection
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter your name..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin pulse-glow" />
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Send
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{data && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5 rounded-lg" />
|
||||
<pre className="relative p-4 rounded-lg text-sm font-mono overflow-x-auto">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Users CRUD Example
|
||||
// ============================================================================
|
||||
|
||||
function UsersExample() {
|
||||
const [newName, setNewName] = useState("");
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState("");
|
||||
const [editEmail, setEditEmail] = useState("");
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const { data: users, isLoading } = trpc.users.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.users.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.users.getAll.invalidate();
|
||||
setNewName("");
|
||||
setNewEmail("");
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = trpc.users.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.users.getAll.invalidate();
|
||||
setEditingId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.users.delete.useMutation({
|
||||
onSuccess: () => utils.users.getAll.invalidate(),
|
||||
});
|
||||
|
||||
const startEditing = (user: { id: number; name: string; email: string }) => {
|
||||
setEditingId(user.id);
|
||||
setEditName(user.name);
|
||||
setEditEmail(user.email);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="card-hover glass-card overflow-hidden">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-secondary/10 glow-secondary">
|
||||
<Users className="h-5 w-5 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">Users</CardTitle>
|
||||
<CardDescription>Full CRUD operations example</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Create Form */}
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="Name"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
createMutation.mutate({ name: newName, email: newEmail })
|
||||
}
|
||||
disabled={!newName || !newEmail || createMutation.isPending}
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Users List */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary pulse-glow" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{users?.length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-4">
|
||||
No users yet. Create one above!
|
||||
</p>
|
||||
)}
|
||||
{users?.map((user) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className="item-row flex items-center gap-3 p-4 rounded-lg bg-muted/30 border border-border/50"
|
||||
>
|
||||
{editingId === user.id ? (
|
||||
<>
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={editEmail}
|
||||
onChange={(e) => setEditEmail(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-green-500 hover:text-green-600 hover:bg-green-500/10"
|
||||
onClick={() =>
|
||||
updateMutation.mutate({
|
||||
id: user.id,
|
||||
name: editName,
|
||||
email: editEmail,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setEditingId(null)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center text-white text-sm font-medium">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{user.name}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
onClick={() => startEditing(user)}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => deleteMutation.mutate({ id: user.id })}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Posts Example
|
||||
// ============================================================================
|
||||
|
||||
function PostsExample() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [authorId, setAuthorId] = useState("1");
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const { data: posts, isLoading } = trpc.posts.getAll.useQuery();
|
||||
|
||||
const createMutation = trpc.posts.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.posts.getAll.invalidate();
|
||||
setTitle("");
|
||||
setContent("");
|
||||
},
|
||||
});
|
||||
|
||||
const publishMutation = trpc.posts.publish.useMutation({
|
||||
onSuccess: () => utils.posts.getAll.invalidate(),
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.posts.delete.useMutation({
|
||||
onSuccess: () => utils.posts.getAll.invalidate(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="card-hover glass-card overflow-hidden">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-accent/10 glow-accent">
|
||||
<FileText className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">Posts</CardTitle>
|
||||
<CardDescription>With publish action and filtering</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Create Form */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Post title..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="w-28">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">
|
||||
Author ID
|
||||
</Label>
|
||||
<Input
|
||||
value={authorId}
|
||||
onChange={(e) => setAuthorId(e.target.value)}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Write something amazing..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
createMutation.mutate({
|
||||
title,
|
||||
content,
|
||||
authorId: parseInt(authorId),
|
||||
published: false,
|
||||
})
|
||||
}
|
||||
disabled={!title || !content || createMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Posts List */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-accent pulse-glow" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{posts?.length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-4">
|
||||
No posts yet. Create your first post!
|
||||
</p>
|
||||
)}
|
||||
{posts?.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="item-row p-4 rounded-lg bg-muted/30 border border-border/50"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold truncate">{post.title}</h3>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
post.published ? "badge-success" : "badge-warning"
|
||||
}`}
|
||||
>
|
||||
{post.published ? "Published" : "Draft"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{post.content}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{!post.published && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-secondary border-secondary/30 hover:bg-secondary/10"
|
||||
onClick={() => publishMutation.mutate({ id: post.id })}
|
||||
>
|
||||
<Sparkles className="h-3 w-3 mr-1" />
|
||||
Publish
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => deleteMutation.mutate({ id: post.id })}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user