In Metropolis Quantum, advanced research laboratories developed modular, prefabricated building elements that can be easily combined and customized as needed. Thanks to this, architects can quickly create buildings with a consistent aesthetic while maintaining flexibility in designing unique spaces. A similar role in the world of frontend development is played by the Shadcn/UI library - a collection of carefully designed and accessible user interface components that can be easily integrated and customized to meet project needs.
Shadcn/UI is a collection of UI components built with Radix UI, Tailwind CSS, and React in mind. Unlike traditional component libraries, Shadcn/UI is not installed as an npm dependency. Instead, components are copied directly into your project, giving you full control over the code and enabling easy modifications as needed.
To get started with Shadcn/UI in a Next.js 15 project, you need to follow a few steps:
1npx shadcn-ui@latest initAfter running this command, the CLI will ask you to answer a few questions:
The CLI will automatically update the Tailwind CSS configuration file. It is worth checking the contents of the
tailwind.config.js file: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}The CLI will also add CSS variables to the
globals.css file: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}Now you can install selected components using the 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 }The CLI will also automatically add a
lib/utils.ts file containing utility functions: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 offers many components, from simple buttons to advanced interaction components. Here are some of the most important ones:
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: "Please enter a valid email address.",
25 }),
26 password: z.string().min(8, {
27 message: "Password must contain at least 8 characters.",
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 // In a real application, we would call the login API here
48 console.log(values)
49
50 // Simulating delay
51 await new Promise(resolve => setTimeout(resolve, 1000))
52
53 toast({
54 title: "Logged in successfully",
55 description: "Redirecting you to the main dashboard",
56 })
57
58 router.push("/dashboard")
59 } catch (error) {
60 toast({
61 variant: "destructive",
62 title: "An error occurred",
63 description: "Unable to log in. Check your credentials and try again.",
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 Enter your credentials to log in to the Metropolis Quantum panel
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>Password</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 ? "Logging in..." : "Log in"}
108 </Button>
109 </form>
110 </Form>
111 <div className="text-center text-sm">
112 <a href="/forgot-password" className="text-primary hover:underline">
113 Forgot your password?
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">Overview</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">Total Revenue</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</div>
41 <p className="text-xs text-muted-foreground">
42 +20.1% from last month
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% from last month
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">Sales</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% from last month
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">Active Users</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 from last month
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>Overview</CardTitle>
123 </CardHeader>
124 <CardContent className="pl-2">
125 {/* A chart could go here */}
126 <div className="h-[200px] bg-muted rounded-md flex items-center justify-center">
127 Revenue Chart
128 </div>
129 </CardContent>
130 </Card>
131 <Card className="col-span-3">
132 <CardHeader>
133 <CardTitle>Ostatnie Transakcje</CardTitle>
134 <CardDescription>
135 24 transactions completed this month.
136 </CardDescription>
137 </CardHeader>
138 <CardContent>
139 <div className="space-y-8">
140 {/* Example transaction */}
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</div>
161 </div>
162 {/* More transactions */}
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 Detailed traffic and conversion analysis.
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 Browse and generate activity reports.
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">Monthly Report (April 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 Manage your account settings.
223 </CardDescription>
224 </CardHeader>
225 <CardContent className="space-y-4">
226 {/* Example settings */}
227 <div className="space-y-1">
228 <p className="text-sm font-medium">Powiadomienia Email</p>
229 <p className="text-xs text-muted-foreground">Receive notifications about important updates.</p>
230 </div>
231 {/* More settings */}
232 </CardContent>
233 <CardFooter>
234 <Button>Save 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: `Project "${formData.name}" has been successfully created.`,
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>Create New Project</DialogTitle>
74 <DialogDescription>
75 Enter project details. Click save when you are done.
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="Describe your project"
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="Choose a category" />
109 </SelectTrigger>
110 <SelectContent>
111 <SelectItem value="development">Development</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">Medium</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 Cancel
142 </Button>
143 <Button type="submit" disabled={isLoading}>
144 {isLoading ? "Zapisywanie..." : "Save projekt"}
145 </Button>
146 </DialogFooter>
147 </form>
148 </DialogContent>
149 </Dialog>
150 )
151}One of the greatest advantages of Shadcn/UI is the ease of customizing components. Since the components are part of your code, you can modify them as needed.
You can modify the appearance of components in several ways:
globals.css1:root {
2 --primary: 240 5.9% 10%; /* Zmiana koloru primary */
3 --radius: 0.75rem; /* Changing corner rounding */
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 // Other definitions
17 },
18 }
19)
20
21// Update the interface to include the new variant
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// Export remaining components
33export { CardContent, CardFooter, CardHeader }You can also extend the functionality of components:
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"With Shadcn/UI, you can easily create and manage your own design systems:
1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5
6@layer base {
7 /* Default theme (light) */
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 /* Other variables */
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 // Remove previous theme classes
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">Toggle theme</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}Maintain a clear directory structure:
1components/
2├── ui/ # Podstawowe komponenty Shadcn/UI
3│ ├── button.tsx
4│ ├── card.tsx
5│ └──
6├── quantum/ # Your own business components
7│ ├── quantum-button.tsx
8│ ├── quantum-card.tsx
9│ └──
10└── blocks/ # Complex interface blocks
11 ├── dashboard-stats.tsx
12 ├── user-profile-form.tsx
13 └──Maintain consistent naming conventions for your own components:
Button - podstawowy komponent Shadcn/UIQuantumButton - custom extension of a base componentUserProfileForm - complex business componentUse the Shadcn/UI CLI to generate new components:
1npx shadcn-ui@latest add alertFor frequently used combinations, create your own higher-level components:
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}Use CSS variables to manage themes:
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 integrates well with libraries such as React Hook Form and 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>Username</FormLabel>
49 <FormControl>
50 <Input placeholder="Enter your username" {...field} />
51 </FormControl>
52 <FormDescription>
53 The name that will be displayed on your profile.
54 </FormDescription>
55 <FormMessage />
56 </FormItem>
57 )}
58 />
59 {/* Other form fields */}
60 <Button type="submit">Sign Up</Button>
61 </form>
62 </Form>
63 )
64}Shadcn/UI can be easily integrated with animation libraries:
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 works well with state management libraries:
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">Toggle theme</span>
50 </Button>
51 )
52}Shadcn/UI is a powerful tool for creating beautiful, accessible, and consistent user interfaces in Next.js 15. Instead of being a black box that is hard to customize, Shadcn/UI provides high-quality, modifiable components that become part of your code.
The most important advantages of Shadcn/UI:
Unlike traditional UI libraries that may limit your creativity and make it difficult to customize to unique project needs, Shadcn/UI provides solid foundations on which you can build your own unique user experiences - just like the modular building system in Metropolis Quantum allows architects to create unique buildings while maintaining the consistent aesthetic of the city.
In the next chapter, we will look at the Image component and image optimization in Next.js 15, which will allow us to further improve the performance and user experience of our applications.