W Metropolis Quantum, zaawansowane laboratoria badawcze opracowały modułowe, prefabrykowane elementy budowlane, które można łatwo łączyć i dostosowywać według potrzeb. Dzięki temu architekci mogą szybko tworzyć budynki o spójnej estetyce, jednocześnie zachowując elastyczność w projektowaniu unikalnych przestrzeni. Podobną rolę w świecie programowania frontendowego pełni biblioteka Shadcn/UI - kolekcja starannie zaprojektowanych i dostępnych komponentów interfejsu użytkownika, które można łatwo integrować i dostosowywać do potrzeb projektu.
Shadcn/UI to zbiór komponentów UI zbudowanych z myślą o Radix UI, Tailwind CSS i React. W przeciwieństwie do tradycyjnych bibliotek komponentów, Shadcn/UI nie jest instalowane jako zależność npm. Zamiast tego, komponenty są kopiowane bezpośrednio do twojego projektu, co daje ci pełną kontrolę nad kodem i umożliwia łatwe modyfikacje według potrzeb.
Aby rozpocząć pracę z Shadcn/UI w projekcie Next.js 15, musisz wykonać kilka kroków:
1npx shadcn-ui@latest initPo uruchomieniu tego polecenia, CLI poprosi cię o odpowiedź na kilka pytań:
CLI automatycznie zaktualizuje plik konfiguracyjny Tailwind CSS. Warto sprawdzić zawartość pliku
tailwind.config.js:1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 darkMode: ["class"],
5 content: [
6 './pages/**/*.{js,ts,jsx,tsx,mdx}',
7 './components/**/*.{js,ts,jsx,tsx,mdx}',
8 './app/**/*.{js,ts,jsx,tsx,mdx}',
9 ],
10 theme: {
11 container: {
12 center: true,
13 padding: "2rem",
14 screens: {
15 "2xl": "1400px",
16 },
17 },
18 extend: {
19 colors: {
20 border: "hsl(var(--border))",
21 input: "hsl(var(--input))",
22 ring: "hsl(var(--ring))",
23 background: "hsl(var(--background))",
24 foreground: "hsl(var(--foreground))",
25 primary: {
26 DEFAULT: "hsl(var(--primary))",
27 foreground: "hsl(var(--primary-foreground))",
28 },
29 secondary: {
30 DEFAULT: "hsl(var(--secondary))",
31 foreground: "hsl(var(--secondary-foreground))",
32 },
33 destructive: {
34 DEFAULT: "hsl(var(--destructive))",
35 foreground: "hsl(var(--destructive-foreground))",
36 },
37 muted: {
38 DEFAULT: "hsl(var(--muted))",
39 foreground: "hsl(var(--muted-foreground))",
40 },
41 accent: {
42 DEFAULT: "hsl(var(--accent))",
43 foreground: "hsl(var(--accent-foreground))",
44 },
45 popover: {
46 DEFAULT: "hsl(var(--popover))",
47 foreground: "hsl(var(--popover-foreground))",
48 },
49 card: {
50 DEFAULT: "hsl(var(--card))",
51 foreground: "hsl(var(--card-foreground))",
52 },
53 },
54 borderRadius: {
55 lg: "var(--radius)",
56 md: "calc(var(--radius) - 2px)",
57 sm: "calc(var(--radius) - 4px)",
58 },
59 keyframes: {
60 "accordion-down": {
61 from: { height: 0 },
62 to: { height: "var(--radix-accordion-content-height)" },
63 },
64 "accordion-up": {
65 from: { height: "var(--radix-accordion-content-height)" },
66 to: { height: 0 },
67 },
68 },
69 animation: {
70 "accordion-down": "accordion-down 0.2s ease-out",
71 "accordion-up": "accordion-up 0.2s ease-out",
72 },
73 },
74 },
75 plugins: [require("tailwindcss-animate")],
76}CLI doda również zmienne CSS do pliku
globals.css:1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4
5@layer base {
6 :root {
7 --background: 0 0% 100%;
8 --foreground: 222.2 84% 4.9%;
9
10 --card: 0 0% 100%;
11 --card-foreground: 222.2 84% 4.9%;
12
13 --popover: 0 0% 100%;
14 --popover-foreground: 222.2 84% 4.9%;
15
16 --primary: 222.2 47.4% 11.2%;
17 --primary-foreground: 210 40% 98%;
18
19 --secondary: 210 40% 96.1%;
20 --secondary-foreground: 222.2 47.4% 11.2%;
21
22 --muted: 210 40% 96.1%;
23 --muted-foreground: 215.4 16.3% 46.9%;
24
25 --accent: 210 40% 96.1%;
26 --accent-foreground: 222.2 47.4% 11.2%;
27
28 --destructive: 0 84.2% 60.2%;
29 --destructive-foreground: 210 40% 98%;
30
31 --border: 214.3 31.8% 91.4%;
32 --input: 214.3 31.8% 91.4%;
33 --ring: 222.2 84% 4.9%;
34
35 --radius: 0.5rem;
36 }
37
38 .dark {
39 --background: 222.2 84% 4.9%;
40 --foreground: 210 40% 98%;
41
42 --card: 222.2 84% 4.9%;
43 --card-foreground: 210 40% 98%;
44
45 --popover: 222.2 84% 4.9%;
46 --popover-foreground: 210 40% 98%;
47
48 --primary: 210 40% 98%;
49 --primary-foreground: 222.2 47.4% 11.2%;
50
51 --secondary: 217.2 32.6% 17.5%;
52 --secondary-foreground: 210 40% 98%;
53
54 --muted: 217.2 32.6% 17.5%;
55 --muted-foreground: 215 20.2% 65.1%;
56
57 --accent: 217.2 32.6% 17.5%;
58 --accent-foreground: 210 40% 98%;
59
60 --destructive: 0 62.8% 30.6%;
61 --destructive-foreground: 210 40% 98%;
62
63 --border: 217.2 32.6% 17.5%;
64 --input: 217.2 32.6% 17.5%;
65 --ring: 212.7 26.8% 83.9%;
66 }
67}
68
69@layer base {
70 * {
71 @apply border-border;
72 }
73 body {
74 @apply bg-background text-foreground;
75 }
76}Teraz możesz instalować wybrane komponenty za pomocą CLI:
1npx shadcn-ui@latest add buttonTo polecenie doda komponent Button do katalogu
components/ui:1// components/ui/button.tsx
2import * as React from "react"
3import { Slot } from "@radix-ui/react-slot"
4import { cva, type VariantProps } from "class-variance-authority"
5
6import { cn } from "@/lib/utils"
7
8const buttonVariants = cva(
9 "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
10 {
11 variants: {
12 variant: {
13 default: "bg-primary text-primary-foreground hover:bg-primary/90",
14 destructive:
15 "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16 outline:
17 "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
18 secondary:
19 "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20 ghost: "hover:bg-accent hover:text-accent-foreground",
21 link: "text-primary underline-offset-4 hover:underline",
22 },
23 size: {
24 default: "h-10 px-4 py-2",
25 sm: "h-9 rounded-md px-3",
26 lg: "h-11 rounded-md px-8",
27 icon: "h-10 w-10",
28 },
29 },
30 defaultVariants: {
31 variant: "default",
32 size: "default",
33 },
34 }
35)
36
37export interface ButtonProps
38 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39 VariantProps<typeof buttonVariants> {
40 asChild?: boolean
41}
42
43const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44 ({ className, variant, size, asChild = false, ...props }, ref) => {
45 const Comp = asChild ? Slot : "button"
46 return (
47 <Comp
48 className={cn(buttonVariants({ variant, size, className }))}
49 ref={ref}
50 {...props}
51 />
52 )
53 }
54)
55Button.displayName = "Button"
56
57export { Button, buttonVariants }CLI automatycznie doda również plik
lib/utils.ts, który zawiera pomocnicze funkcje:1// lib/utils.ts
2import { type ClassValue, clsx } from "clsx"
3import { twMerge } from "tailwind-merge"
4
5export function cn(...inputs: ClassValue[]) {
6 return twMerge(clsx(inputs))
7}Shadcn/UI oferuje wiele komponentów, od prostych przycisków po zaawansowane komponenty interakcji. Oto kilka najważniejszych:
1// app/login/page.tsx
2"use client"
3
4import { useState } from "react"
5import { useRouter } from "next/navigation"
6import { zodResolver } from "@hookform/resolvers/zod"
7import { useForm } from "react-hook-form"
8import * as z from "zod"
9
10import { Button } from "@/components/ui/button"
11import {
12 Form,
13 FormControl,
14 FormField,
15 FormItem,
16 FormLabel,
17 FormMessage,
18} from "@/components/ui/form"
19import { Input } from "@/components/ui/input"
20import { toast } from "@/components/ui/use-toast"
21
22const formSchema = z.object({
23 email: z.string().email({
24 message: "Wprowadź poprawny adres email.",
25 }),
26 password: z.string().min(8, {
27 message: "Hasło musi zawierać co najmniej 8 znaków.",
28 }),
29})
30
31export default function LoginPage() {
32 const router = useRouter()
33 const [isLoading, setIsLoading] = useState(false)
34
35 const form = useForm<z.infer<typeof formSchema>>({
36 resolver: zodResolver(formSchema),
37 defaultValues: {
38 email: "",
39 password: "",
40 },
41 })
42
43 async function onSubmit(values: z.infer<typeof formSchema>) {
44 setIsLoading(true)
45
46 try {
47 // W rzeczywistej aplikacji wywołalibyśmy tutaj API logowania
48 console.log(values)
49
50 // Symulacja opóźnienia
51 await new Promise(resolve => setTimeout(resolve, 1000))
52
53 toast({
54 title: "Zalogowano pomyślnie",
55 description: "Przekierowujemy Cię do panelu głównego",
56 })
57
58 router.push("/dashboard")
59 } catch (error) {
60 toast({
61 variant: "destructive",
62 title: "Wystąpił błąd",
63 description: "Nie można zalogować. Sprawdź dane logowania i spróbuj ponownie.",
64 })
65 } finally {
66 setIsLoading(false)
67 }
68 }
69
70 return (
71 <div className="mx-auto max-w-md space-y-6 p-6">
72 <div className="space-y-2 text-center">
73 <h1 className="text-3xl font-bold">Logowanie</h1>
74 <p className="text-gray-500 dark:text-gray-400">
75 Wprowadź swoje dane, aby zalogować się do panelu Metropolis Quantum
76 </p>
77 </div>
78 <Form {...form}>
79 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
80 <FormField
81 control={form.control}
82 name="email"
83 render={({ field }) => (
84 <FormItem>
85 <FormLabel>Email</FormLabel>
86 <FormControl>
87 <Input type="email" placeholder="email@example.com" {...field} />
88 </FormControl>
89 <FormMessage />
90 </FormItem>
91 )}
92 />
93 <FormField
94 control={form.control}
95 name="password"
96 render={({ field }) => (
97 <FormItem>
98 <FormLabel>Hasło</FormLabel>
99 <FormControl>
100 <Input type="password" {...field} />
101 </FormControl>
102 <FormMessage />
103 </FormItem>
104 )}
105 />
106 <Button type="submit" className="w-full" disabled={isLoading}>
107 {isLoading ? "Logowanie..." : "Zaloguj się"}
108 </Button>
109 </form>
110 </Form>
111 <div className="text-center text-sm">
112 <a href="/forgot-password" className="text-primary hover:underline">
113 Nie pamiętasz hasła?
114 </a>
115 </div>
116 </div>
117 )
118}1// app/dashboard/page.tsx
2"use client"
3
4import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
5import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
6import { Button } from "@/components/ui/button"
7
8export default function DashboardPage() {
9 return (
10 <div className="container mx-auto py-10">
11 <h1 className="text-3xl font-bold mb-6">Dashboard Metropolis Quantum</h1>
12
13 <Tabs defaultValue="overview" className="space-y-4">
14 <TabsList className="grid w-full grid-cols-4">
15 <TabsTrigger value="overview">Przegląd</TabsTrigger>
16 <TabsTrigger value="analytics">Analityka</TabsTrigger>
17 <TabsTrigger value="reports">Raporty</TabsTrigger>
18 <TabsTrigger value="settings">Ustawienia</TabsTrigger>
19 </TabsList>
20
21 <TabsContent value="overview" className="space-y-4">
22 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
23 <Card>
24 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
25 <CardTitle className="text-sm font-medium">Całkowity Przychód</CardTitle>
26 <svg
27 xmlns="http://www.w3.org/2000/svg"
28 viewBox="0 0 24 24"
29 fill="none"
30 stroke="currentColor"
31 strokeLinecap="round"
32 strokeLinejoin="round"
33 strokeWidth="2"
34 className="h-4 w-4 text-muted-foreground"
35 >
36 <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
37 </svg>
38 </CardHeader>
39 <CardContent>
40 <div className="text-2xl font-bold">45,231.89 zł</div>
41 <p className="text-xs text-muted-foreground">
42 +20.1% od ostatniego miesiąca
43 </p>
44 </CardContent>
45 </Card>
46 <Card>
47 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
48 <CardTitle className="text-sm font-medium">Subskrypcje</CardTitle>
49 <svg
50 xmlns="http://www.w3.org/2000/svg"
51 viewBox="0 0 24 24"
52 fill="none"
53 stroke="currentColor"
54 strokeLinecap="round"
55 strokeLinejoin="round"
56 strokeWidth="2"
57 className="h-4 w-4 text-muted-foreground"
58 >
59 <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
60 <circle cx="9" cy="7" r="4" />
61 <path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
62 </svg>
63 </CardHeader>
64 <CardContent>
65 <div className="text-2xl font-bold">+2350</div>
66 <p className="text-xs text-muted-foreground">
67 +180.1% od ostatniego miesiąca
68 </p>
69 </CardContent>
70 </Card>
71 <Card>
72 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
73 <CardTitle className="text-sm font-medium">Sprzedaże</CardTitle>
74 <svg
75 xmlns="http://www.w3.org/2000/svg"
76 viewBox="0 0 24 24"
77 fill="none"
78 stroke="currentColor"
79 strokeLinecap="round"
80 strokeLinejoin="round"
81 strokeWidth="2"
82 className="h-4 w-4 text-muted-foreground"
83 >
84 <rect width="20" height="14" x="2" y="5" rx="2" />
85 <path d="M2 10h20" />
86 </svg>
87 </CardHeader>
88 <CardContent>
89 <div className="text-2xl font-bold">+12,234</div>
90 <p className="text-xs text-muted-foreground">
91 +19% od ostatniego miesiąca
92 </p>
93 </CardContent>
94 </Card>
95 <Card>
96 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
97 <CardTitle className="text-sm font-medium">Aktywni Użytkownicy</CardTitle>
98 <svg
99 xmlns="http://www.w3.org/2000/svg"
100 viewBox="0 0 24 24"
101 fill="none"
102 stroke="currentColor"
103 strokeLinecap="round"
104 strokeLinejoin="round"
105 strokeWidth="2"
106 className="h-4 w-4 text-muted-foreground"
107 >
108 <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
109 </svg>
110 </CardHeader>
111 <CardContent>
112 <div className="text-2xl font-bold">+573</div>
113 <p className="text-xs text-muted-foreground">
114 +201 od ostatniego miesiąca
115 </p>
116 </CardContent>
117 </Card>
118 </div>
119 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
120 <Card className="col-span-4">
121 <CardHeader>
122 <CardTitle>Przegląd</CardTitle>
123 </CardHeader>
124 <CardContent className="pl-2">
125 {/* Tutaj mógłby być wykres */}
126 <div className="h-[200px] bg-muted rounded-md flex items-center justify-center">
127 Wykres Przychodów
128 </div>
129 </CardContent>
130 </Card>
131 <Card className="col-span-3">
132 <CardHeader>
133 <CardTitle>Ostatnie Transakcje</CardTitle>
134 <CardDescription>
135 Wykonano 24 transakcje w tym miesiącu.
136 </CardDescription>
137 </CardHeader>
138 <CardContent>
139 <div className="space-y-8">
140 {/* Przykładowa transakcja */}
141 <div className="flex items-center">
142 <div className="mr-4 rounded-full bg-primary/10 p-2">
143 <svg
144 xmlns="http://www.w3.org/2000/svg"
145 viewBox="0 0 24 24"
146 fill="none"
147 stroke="currentColor"
148 strokeLinecap="round"
149 strokeLinejoin="round"
150 strokeWidth="2"
151 className="h-4 w-4 text-primary"
152 >
153 <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
154 </svg>
155 </div>
156 <div className="space-y-1">
157 <p className="text-sm font-medium">Quantum Plan (Roczny)</p>
158 <p className="text-xs text-muted-foreground">14 kwietnia 2024</p>
159 </div>
160 <div className="ml-auto font-medium">+4,999.00 zł</div>
161 </div>
162 {/* Więcej transakcji */}
163 </div>
164 </CardContent>
165 </Card>
166 </div>
167 </TabsContent>
168
169 <TabsContent value="analytics" className="space-y-4">
170 <Card className="col-span-4">
171 <CardHeader>
172 <CardTitle>Analityka</CardTitle>
173 <CardDescription>
174 Szczegółowa analiza ruchu i konwersji.
175 </CardDescription>
176 </CardHeader>
177 <CardContent>
178 <div className="h-[300px] bg-muted rounded-md flex items-center justify-center">
179 Dane Analityczne
180 </div>
181 </CardContent>
182 </Card>
183 </TabsContent>
184
185 <TabsContent value="reports" className="space-y-4">
186 <Card>
187 <CardHeader>
188 <CardTitle>Raporty</CardTitle>
189 <CardDescription>
190 Przeglądaj i generuj raporty z działalności.
191 </CardDescription>
192 </CardHeader>
193 <CardContent className="space-y-4">
194 <div className="space-y-2">
195 <div className="flex items-center justify-between">
196 <div>
197 <p className="text-sm font-medium">Raport Miesięczny (Kwiecień 2024)</p>
198 <p className="text-xs text-muted-foreground">Utworzony 01.05.2024</p>
199 </div>
200 <Button variant="outline" size="sm">Pobierz</Button>
201 </div>
202 <div className="flex items-center justify-between">
203 <div>
204 <p className="text-sm font-medium">Raport Kwartalny (Q1 2024)</p>
205 <p className="text-xs text-muted-foreground">Utworzony 05.04.2024</p>
206 </div>
207 <Button variant="outline" size="sm">Pobierz</Button>
208 </div>
209 </div>
210 </CardContent>
211 <CardFooter>
212 <Button>Generuj Nowy Raport</Button>
213 </CardFooter>
214 </Card>
215 </TabsContent>
216
217 <TabsContent value="settings" className="space-y-4">
218 <Card>
219 <CardHeader>
220 <CardTitle>Ustawienia</CardTitle>
221 <CardDescription>
222 Zarządzaj ustawieniami swojego konta.
223 </CardDescription>
224 </CardHeader>
225 <CardContent className="space-y-4">
226 {/* Przykładowe ustawienia */}
227 <div className="space-y-1">
228 <p className="text-sm font-medium">Powiadomienia Email</p>
229 <p className="text-xs text-muted-foreground">Otrzymuj powiadomienia o ważnych aktualizacjach.</p>
230 </div>
231 {/* Więcej ustawień */}
232 </CardContent>
233 <CardFooter>
234 <Button>Zapisz Zmiany</Button>
235 </CardFooter>
236 </Card>
237 </TabsContent>
238 </Tabs>
239 </div>
240 )
241}1// components/quantum-project-dialog.tsx
2"use client"
3
4import { useState } from "react"
5import { Button } from "@/components/ui/button"
6import {
7 Dialog,
8 DialogContent,
9 DialogDescription,
10 DialogFooter,
11 DialogHeader,
12 DialogTitle,
13 DialogTrigger,
14} from "@/components/ui/dialog"
15import { Input } from "@/components/ui/input"
16import { Label } from "@/components/ui/label"
17import { Textarea } from "@/components/ui/textarea"
18import {
19 Select,
20 SelectContent,
21 SelectItem,
22 SelectTrigger,
23 SelectValue,
24} from "@/components/ui/select"
25import { toast } from "@/components/ui/use-toast"
26
27export function QuantumProjectDialog() {
28 const [open, setOpen] = useState(false)
29 const [isLoading, setIsLoading] = useState(false)
30
31 const [formData, setFormData] = useState({
32 name: "",
33 description: "",
34 category: "",
35 priority: "",
36 })
37
38 const handleChange = (field: string, value: string) => {
39 setFormData((prev) => ({ ...prev, [field]: value }))
40 }
41
42 const handleSubmit = async (e: React.FormEvent) => {
43 e.preventDefault()
44 setIsLoading(true)
45
46 // Symulacja zapisywania danych
47 await new Promise(resolve => setTimeout(resolve, 1000))
48
49 toast({
50 title: "Projekt utworzony",
51 description: `Projekt "${formData.name}" został pomyślnie utworzony.`,
52 })
53
54 setIsLoading(false)
55 setOpen(false)
56 // Reset formularza
57 setFormData({
58 name: "",
59 description: "",
60 category: "",
61 priority: "",
62 })
63 }
64
65 return (
66 <Dialog open={open} onOpenChange={setOpen}>
67 <DialogTrigger asChild>
68 <Button variant="default">Nowy Projekt</Button>
69 </DialogTrigger>
70 <DialogContent className="sm:max-w-[425px]">
71 <form onSubmit={handleSubmit}>
72 <DialogHeader>
73 <DialogTitle>Utwórz nowy projekt</DialogTitle>
74 <DialogDescription>
75 Wprowadź szczegóły projektu. Kliknij zapisz, gdy skończysz.
76 </DialogDescription>
77 </DialogHeader>
78 <div className="grid gap-4 py-4">
79 <div className="grid gap-2">
80 <Label htmlFor="name">Nazwa projektu</Label>
81 <Input
82 id="name"
83 value={formData.name}
84 onChange={(e) => handleChange("name", e.target.value)}
85 placeholder="Nazwa projektu"
86 required
87 />
88 </div>
89 <div className="grid gap-2">
90 <Label htmlFor="description">Opis</Label>
91 <Textarea
92 id="description"
93 value={formData.description}
94 onChange={(e) => handleChange("description", e.target.value)}
95 placeholder="Opisz swój projekt"
96 required
97 />
98 </div>
99 <div className="grid grid-cols-2 gap-4">
100 <div className="grid gap-2">
101 <Label htmlFor="category">Kategoria</Label>
102 <Select
103 value={formData.category}
104 onValueChange={(value) => handleChange("category", value)}
105 required
106 >
107 <SelectTrigger id="category">
108 <SelectValue placeholder="Wybierz kategorię" />
109 </SelectTrigger>
110 <SelectContent>
111 <SelectItem value="development">Rozwój</SelectItem>
112 <SelectItem value="research">Badania</SelectItem>
113 <SelectItem value="infrastructure">Infrastruktura</SelectItem>
114 <SelectItem value="marketing">Marketing</SelectItem>
115 <SelectItem value="other">Inne</SelectItem>
116 </SelectContent>
117 </Select>
118 </div>
119 <div className="grid gap-2">
120 <Label htmlFor="priority">Priorytet</Label>
121 <Select
122 value={formData.priority}
123 onValueChange={(value) => handleChange("priority", value)}
124 required
125 >
126 <SelectTrigger id="priority">
127 <SelectValue placeholder="Wybierz priorytet" />
128 </SelectTrigger>
129 <SelectContent>
130 <SelectItem value="low">Niski</SelectItem>
131 <SelectItem value="medium">Średni</SelectItem>
132 <SelectItem value="high">Wysoki</SelectItem>
133 <SelectItem value="critical">Krytyczny</SelectItem>
134 </SelectContent>
135 </Select>
136 </div>
137 </div>
138 </div>
139 <DialogFooter>
140 <Button type="button" variant="outline" onClick={() => setOpen(false)}>
141 Anuluj
142 </Button>
143 <Button type="submit" disabled={isLoading}>
144 {isLoading ? "Zapisywanie..." : "Zapisz projekt"}
145 </Button>
146 </DialogFooter>
147 </form>
148 </DialogContent>
149 </Dialog>
150 )
151}Jedną z największych zalet Shadcn/UI jest łatwość dostosowywania komponentów. Ponieważ komponenty są częścią twojego kodu, możesz je modyfikować według potrzeb.
Możesz zmodyfikować wygląd komponentów na różne sposoby:
globals.css1:root {
2 --primary: 240 5.9% 10%; /* Zmiana koloru primary */
3 --radius: 0.75rem; /* Zmiana zaokrąglenia narożników */
4}1// components/ui/button.tsx
2const buttonVariants = cva(
3 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
4 {
5 variants: {
6 variant: {
7 default: "bg-primary text-primary-foreground hover:bg-primary/90",
8 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
9 outline: "border border-input hover:bg-accent hover:text-accent-foreground",
10 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
11 ghost: "hover:bg-accent hover:text-accent-foreground",
12 link: "underline-offset-4 hover:underline text-primary",
13 // Dodajemy nowy wariant
14 gradient: "bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90",
15 },
16 // Pozostałe definicje
17 },
18 }
19)
20
21// Aktualizacja interfejsu, aby zawierał nowy wariant
22export interface ButtonProps
23 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
24 VariantProps<typeof buttonVariants> {
25 asChild?: boolean
26}1// components/quantum/quantum-card.tsx
2import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
3import { cn } from "@/lib/utils"
4
5interface QuantumCardProps {
6 className?: string
7 children: React.ReactNode
8 gradient?: boolean
9 hoverable?: boolean
10}
11
12export function QuantumCard({
13 className,
14 children,
15 gradient = false,
16 hoverable = false,
17}: QuantumCardProps) {
18 return (
19 <Card
20 className={cn(
21 "overflow-hidden",
22 hoverable && "transition-transform hover:-translate-y-1 hover:shadow-lg",
23 gradient && "bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950 dark:to-indigo-950",
24 className
25 )}
26 >
27 {children}
28 </Card>
29 )
30}
31
32// Eksportujemy pozostałe komponenty
33export { CardContent, CardFooter, CardHeader }Możesz również rozszerzyć funkcjonalność komponentów:
1// components/quantum/quantum-button.tsx
2import { forwardRef } from "react"
3import { Button, ButtonProps } from "@/components/ui/button"
4import { Loader2 } from "lucide-react"
5
6interface QuantumButtonProps extends ButtonProps {
7 isLoading?: boolean
8 loadingText?: string
9 icon?: React.ReactNode
10 iconPosition?: "left" | "right"
11}
12
13export const QuantumButton = forwardRef<HTMLButtonElement, QuantumButtonProps>(
14 ({
15 children,
16 isLoading,
17 loadingText,
18 icon,
19 iconPosition = "left",
20 ...props
21 }, ref) => {
22 return (
23 <Button
24 ref={ref}
25 disabled={isLoading || props.disabled}
26 {...props}
27 >
28 {isLoading && (
29 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
30 )}
31 {!isLoading && icon && iconPosition === "left" && (
32 <span className="mr-2">{icon}</span>
33 )}
34 {isLoading && loadingText ? loadingText : children}
35 {!isLoading && icon && iconPosition === "right" && (
36 <span className="ml-2">{icon}</span>
37 )}
38 </Button>
39 )
40 }
41)
42QuantumButton.displayName = "QuantumButton"Z pomocą Shadcn/UI możesz łatwo tworzyć i zarządzać własnymi systemami projektowymi:
1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5
6@layer base {
7 /* Domyślny motyw (jasny) */
8 :root {
9 --background: 0 0% 100%;
10 --foreground: 222.2 84% 4.9%;
11
12 /* Kolory dla marki Quantum */
13 --quantum-blue: 210 100% 50%;
14 --quantum-purple: 270 100% 60%;
15 --quantum-teal: 180 100% 40%;
16
17 /* Pozostałe zmienne */
18 /* ... */
19 }
20
21 /* Ciemny motyw */
22 .dark {
23 --background: 222.2 84% 4.9%;
24 --foreground: 210 40% 98%;
25
26 /* ... */
27 }
28
29 /* Motyw neonowy */
30 .theme-neon {
31 --primary: 120 100% 50%;
32 --secondary: 300 100% 50%;
33 --accent: 60 100% 50%;
34
35 /* ... */
36 }
37
38 /* Motyw vintage */
39 .theme-vintage {
40 --primary: 30 80% 60%;
41 --secondary: 20 60% 40%;
42 --accent: 40 70% 50%;
43
44 /* ... */
45 }
46}1// components/theme-provider.tsx
2"use client"
3
4import { createContext, useContext, useEffect, useState } from "react"
5
6type Theme = "light" | "dark" | "neon" | "vintage" | "system"
7
8type ThemeProviderProps = {
9 children: React.ReactNode
10 defaultTheme?: Theme
11 storageKey?: string
12}
13
14type ThemeProviderState = {
15 theme: Theme
16 setTheme: (theme: Theme) => void
17}
18
19const initialState: ThemeProviderState = {
20 theme: "system",
21 setTheme: () => null,
22}
23
24const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
25
26export function ThemeProvider({
27 children,
28 defaultTheme = "system",
29 storageKey = "quantum-ui-theme",
30 ...props
31}: ThemeProviderProps) {
32 const [theme, setTheme] = useState<Theme>(
33 () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
34 )
35
36 useEffect(() => {
37 const root = window.document.documentElement
38
39 // Usuń poprzednie klasy motywów
40 root.classList.remove("light", "dark", "theme-neon", "theme-vintage")
41
42 if (theme === "system") {
43 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
44 .matches
45 ? "dark"
46 : "light"
47 root.classList.add(systemTheme)
48 return
49 }
50
51 if (theme === "neon") {
52 root.classList.add("dark", "theme-neon")
53 return
54 }
55
56 if (theme === "vintage") {
57 root.classList.add("light", "theme-vintage")
58 return
59 }
60
61 root.classList.add(theme)
62 }, [theme])
63
64 const value = {
65 theme,
66 setTheme: (theme: Theme) => {
67 localStorage.setItem(storageKey, theme)
68 setTheme(theme)
69 },
70 }
71
72 return (
73 <ThemeProviderContext.Provider {...props} value={value}>
74 {children}
75 </ThemeProviderContext.Provider>
76 )
77}
78
79export const useTheme = () => {
80 const context = useContext(ThemeProviderContext)
81
82 if (context === undefined)
83 throw new Error("useTheme must be used within a ThemeProvider")
84
85 return context
86}1// app/layout.tsx
2import { ThemeProvider } from "@/components/theme-provider"
3import "./globals.css"
4
5export default function RootLayout({
6 children,
7}: {
8 children: React.ReactNode
9}) {
10 return (
11 <html lang="pl" suppressHydrationWarning>
12 <body>
13 <ThemeProvider defaultTheme="system" storageKey="quantum-theme">
14 {children}
15 </ThemeProvider>
16 </body>
17 </html>
18 )
19}1// components/theme-switcher.tsx
2"use client"
3
4import { Moon, Sun, Sparkles, Clock } from "lucide-react"
5import { useTheme } from "@/components/theme-provider"
6import { Button } from "@/components/ui/button"
7import {
8 DropdownMenu,
9 DropdownMenuContent,
10 DropdownMenuItem,
11 DropdownMenuTrigger,
12} from "@/components/ui/dropdown-menu"
13
14export function ThemeSwitcher() {
15 const { theme, setTheme } = useTheme()
16
17 return (
18 <DropdownMenu>
19 <DropdownMenuTrigger asChild>
20 <Button variant="outline" size="icon">
21 <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
22 <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
23 <span className="sr-only">Przełącz motyw</span>
24 </Button>
25 </DropdownMenuTrigger>
26 <DropdownMenuContent align="end">
27 <DropdownMenuItem onClick={() => setTheme("light")}>
28 <Sun className="mr-2 h-4 w-4" />
29 <span>Jasny</span>
30 </DropdownMenuItem>
31 <DropdownMenuItem onClick={() => setTheme("dark")}>
32 <Moon className="mr-2 h-4 w-4" />
33 <span>Ciemny</span>
34 </DropdownMenuItem>
35 <DropdownMenuItem onClick={() => setTheme("neon")}>
36 <Sparkles className="mr-2 h-4 w-4" />
37 <span>Neon</span>
38 </DropdownMenuItem>
39 <DropdownMenuItem onClick={() => setTheme("vintage")}>
40 <Clock className="mr-2 h-4 w-4" />
41 <span>Vintage</span>
42 </DropdownMenuItem>
43 <DropdownMenuItem onClick={() => setTheme("system")}>
44 <span>Systemowy</span>
45 </DropdownMenuItem>
46 </DropdownMenuContent>
47 </DropdownMenu>
48 )
49}Utrzymuj przejrzystą strukturę katalogów:
1components/
2├── ui/ # Podstawowe komponenty Shadcn/UI
3│ ├── button.tsx
4│ ├── card.tsx
5│ └──
6├── quantum/ # Twoje własne komponenty biznesowe
7│ ├── quantum-button.tsx
8│ ├── quantum-card.tsx
9│ └──
10└── blocks/ # Złożone bloki interfejsu
11 ├── dashboard-stats.tsx
12 ├── user-profile-form.tsx
13 └──Utrzymuj spójne konwencje nazewnictwa dla własnych komponentów:
Button - podstawowy komponent Shadcn/UIQuantumButton - własne rozszerzenie podstawowego komponentuUserProfileForm - złożony komponent biznesowyUżywaj CLI Shadcn/UI do generowania nowych komponentów:
1npx shadcn-ui@latest add alertDla często używanych kombinacji, twórz własne komponenty wyższego poziomu:
1// components/quantum/quantum-section.tsx
2import { cn } from "@/lib/utils"
3
4interface QuantumSectionProps {
5 title: string
6 description?: string
7 children: React.ReactNode
8 className?: string
9}
10
11export function QuantumSection({
12 title,
13 description,
14 children,
15 className,
16}: QuantumSectionProps) {
17 return (
18 <section className={cn("space-y-4", className)}>
19 <div className="space-y-1">
20 <h2 className="text-2xl font-bold">{title}</h2>
21 {description && (
22 <p className="text-muted-foreground">{description}</p>
23 )}
24 </div>
25 <div>{children}</div>
26 </section>
27 )
28}Używaj zmiennych CSS, aby zarządzać motywami:
1// lib/themes.ts
2export const themes = {
3 default: {
4 primary: "222.2 47.4% 11.2%",
5 // ...inne kolory
6 },
7 quantum: {
8 primary: "240 100% 50%",
9 // ...inne kolory
10 },
11 // ...inne motywy
12}
13
14export function setTheme(theme: keyof typeof themes) {
15 const root = document.documentElement
16 Object.entries(themes[theme]).forEach(([key, value]) => {
17 root.style.setProperty(`--${key}`, value)
18 })
19}Shadcn/UI dobrze integruje się z bibliotekami takimi jak React Hook Form i Zod:
1npx shadcn-ui@latest add form1// components/signup-form.tsx
2"use client"
3
4import { zodResolver } from "@hookform/resolvers/zod"
5import { useForm } from "react-hook-form"
6import * as z from "zod"
7
8import { Button } from "@/components/ui/button"
9import {
10 Form,
11 FormControl,
12 FormDescription,
13 FormField,
14 FormItem,
15 FormLabel,
16 FormMessage,
17} from "@/components/ui/form"
18import { Input } from "@/components/ui/input"
19
20const formSchema = z.object({
21 username: z.string().min(2).max(50),
22 email: z.string().email(),
23 password: z.string().min(8),
24})
25
26export function SignupForm() {
27 const form = useForm<z.infer<typeof formSchema>>({
28 resolver: zodResolver(formSchema),
29 defaultValues: {
30 username: "",
31 email: "",
32 password: "",
33 },
34 })
35
36 function onSubmit(values: z.infer<typeof formSchema>) {
37 console.log(values)
38 }
39
40 return (
41 <Form {...form}>
42 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
43 <FormField
44 control={form.control}
45 name="username"
46 render={({ field }) => (
47 <FormItem>
48 <FormLabel>Nazwa użytkownika</FormLabel>
49 <FormControl>
50 <Input placeholder="Wpisz nazwę użytkownika" {...field} />
51 </FormControl>
52 <FormDescription>
53 Nazwa, która będzie wyświetlana w Twoim profilu.
54 </FormDescription>
55 <FormMessage />
56 </FormItem>
57 )}
58 />
59 {/* Pozostałe pola formularza */}
60 <Button type="submit">Zarejestruj się</Button>
61 </form>
62 </Form>
63 )
64}Shadcn/UI można łatwo zintegrować z bibliotekami animacji:
1// components/animated-card.tsx
2"use client"
3
4import { motion } from "framer-motion"
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
7interface AnimatedCardProps {
8 title: string
9 children: React.ReactNode
10}
11
12export function AnimatedCard({ title, children }: AnimatedCardProps) {
13 return (
14 <motion.div
15 initial={{ opacity: 0, y: 20 }}
16 animate={{ opacity: 1, y: 0 }}
17 transition={{ duration: 0.5 }}
18 >
19 <Card>
20 <CardHeader>
21 <CardTitle>{title}</CardTitle>
22 </CardHeader>
23 <CardContent>{children}</CardContent>
24 </Card>
25 </motion.div>
26 )
27}Shadcn/UI dobrze współpracuje z bibliotekami do zarządzania stanem:
1// store/theme-store.ts
2import { create } from 'zustand'
3
4type Theme = 'light' | 'dark' | 'system'
5
6interface ThemeState {
7 theme: Theme
8 setTheme: (theme: Theme) => void
9}
10
11export const useThemeStore = create<ThemeState>((set) => ({
12 theme: 'system',
13 setTheme: (theme) => set({ theme }),
14}))
15
16// components/theme-toggle.tsx
17"use client"
18
19import { Moon, Sun } from "lucide-react"
20import { useThemeStore } from "@/store/theme-store"
21import { Button } from "@/components/ui/button"
22import { useEffect } from "react"
23
24export function ThemeToggle() {
25 const { theme, setTheme } = useThemeStore()
26
27 useEffect(() => {
28 const root = document.documentElement
29
30 if (theme === "system") {
31 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
32 .matches ? "dark" : "light"
33 root.classList.remove("light", "dark")
34 root.classList.add(systemTheme)
35 } else {
36 root.classList.remove("light", "dark")
37 root.classList.add(theme)
38 }
39 }, [theme])
40
41 return (
42 <Button
43 variant="outline"
44 size="icon"
45 onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
46 >
47 <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
48 <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
49 <span className="sr-only">Przełącz motyw</span>
50 </Button>
51 )
52}Shadcn/UI to potężne narzędzie do tworzenia pięknych, dostępnych i spójnych interfejsów użytkownika w Next.js 15. Zamiast być czarną skrzynką, której trudno dostosować, Shadcn/UI dostarcza wysokiej jakości, modyfikowalne komponenty, które stają się częścią twojego kodu.
Najważniejsze zalety Shadcn/UI:
W przeciwieństwie do tradycyjnych bibliotek UI, które mogą ograniczać twoją kreatywność i utrudniać dostosowanie do unikalnych potrzeb projektu, Shadcn/UI zapewnia solidne fundamenty, na których możesz budować własne, niepowtarzalne doświadczenia użytkownika - podobnie jak modułowy system budowy w Metropolis Quantum pozwala architektom tworzyć unikalne budynki przy jednoczesnym zachowaniu spójnej estetyki miasta.
W następnym rozdziale przyjrzymy się komponentowi Image i optymalizacji obrazów w Next.js 15, co pozwoli nam dalej poprawić wydajność i user experience naszych aplikacji.