ShadcnUI Vaults

Back to Blocks

Advanced Backup Dashboard

Unknown Block

UnknownComponent

Advanced Backup Dashboard

Advanced backup dashboard with tabbed interface, detailed metrics, and comprehensive backup management.

Preview

Full width desktop view

Code

monitoring-3.tsx
1"use client";
2
3import * as React from "react";
4import { useState, useMemo } from "react";
5import {
6  Database,
7  Calendar,
8  MapPin,
9  Clock,
10  CheckCircle,
11  XCircle,
12  AlertCircle,
13  Download,
14  RotateCcw,
15  Trash2,
16  Eye,
17  Filter,
18  Search,
19  ChevronDown,
20  ChevronUp,
21  Server,
22  HardDrive,
23  Activity,
24} from "lucide-react";
25import { cn } from "@/lib/utils";
26import { Button } from "@/components/ui/button";
27import { Badge } from "@/components/ui/badge";
28import {
29  Card,
30  CardContent,
31  CardDescription,
32  CardHeader,
33  CardTitle,
34} from "@/components/ui/card";
35import { Input } from "@/components/ui/input";
36import {
37  Select,
38  SelectContent,
39  SelectItem,
40  SelectTrigger,
41  SelectValue,
42} from "@/components/ui/select";
43import {
44  Table,
45  TableBody,
46  TableCell,
47  TableHead,
48  TableHeader,
49  TableRow,
50} from "@/components/ui/table";
51import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
52
53interface BackupLog {
54  id: string;
55  timestamp: string;
56  level: "info" | "warning" | "error";
57  message: string;
58}
59
60interface DatabaseBackup {
61  id: string;
62  databaseName: string;
63  backupDate: string;
64  backupTime: string;
65  vpsRegion: string;
66  status: "completed" | "failed" | "in_progress" | "pending";
67  size: string;
68  duration: string;
69  type: "full" | "incremental" | "differential";
70  retentionDays: number;
71  compression: boolean;
72  encryption: boolean;
73  logs: BackupLog[];
74  downloadUrl?: string;
75  nextScheduled?: string;
76}
77
78const mockBackups: DatabaseBackup[] = [
79  {
80    id: "backup-001",
81    databaseName: "production-db",
82    backupDate: "2024-01-15",
83    backupTime: "02:00:00",
84    vpsRegion: "us-east-1",
85    status: "completed",
86    size: "2.4 GB",
87    duration: "12m 34s",
88    type: "full",
89    retentionDays: 30,
90    compression: true,
91    encryption: true,
92    nextScheduled: "2024-01-16 02:00:00",
93    logs: [
94      {
95        id: "log-1",
96        timestamp: "2024-01-15 02:00:00",
97        level: "info",
98        message: "Backup started",
99      },
100      {
101        id: "log-2",
102        timestamp: "2024-01-15 02:05:30",
103        level: "info",
104        message: "Database locked for backup",
105      },
106      {
107        id: "log-3",
108        timestamp: "2024-01-15 02:12:34",
109        level: "info",
110        message: "Backup completed successfully",
111      },
112    ],
113  },
114  {
115    id: "backup-002",
116    databaseName: "staging-db",
117    backupDate: "2024-01-15",
118    backupTime: "03:00:00",
119    vpsRegion: "eu-west-1",
120    status: "failed",
121    size: "0 B",
122    duration: "2m 15s",
123    type: "incremental",
124    retentionDays: 7,
125    compression: true,
126    encryption: false,
127    nextScheduled: "2024-01-16 03:00:00",
128    logs: [
129      {
130        id: "log-4",
131        timestamp: "2024-01-15 03:00:00",
132        level: "info",
133        message: "Backup started",
134      },
135      {
136        id: "log-5",
137        timestamp: "2024-01-15 03:01:45",
138        level: "warning",
139        message: "Connection timeout detected",
140      },
141      {
142        id: "log-6",
143        timestamp: "2024-01-15 03:02:15",
144        level: "error",
145        message: "Backup failed: Database connection lost",
146      },
147    ],
148  },
149  {
150    id: "backup-003",
151    databaseName: "analytics-db",
152    backupDate: "2024-01-15",
153    backupTime: "04:00:00",
154    vpsRegion: "ap-southeast-1",
155    status: "in_progress",
156    size: "1.8 GB",
157    duration: "8m 22s",
158    type: "differential",
159    retentionDays: 14,
160    compression: true,
161    encryption: true,
162    nextScheduled: "2024-01-16 04:00:00",
163    logs: [
164      {
165        id: "log-7",
166        timestamp: "2024-01-15 04:00:00",
167        level: "info",
168        message: "Backup started",
169      },
170      {
171        id: "log-8",
172        timestamp: "2024-01-15 04:03:12",
173        level: "info",
174        message: "Processing table: users",
175      },
176      {
177        id: "log-9",
178        timestamp: "2024-01-15 04:06:45",
179        level: "info",
180        message: "Processing table: transactions",
181      },
182    ],
183  },
184  {
185    id: "backup-004",
186    databaseName: "user-db",
187    backupDate: "2024-01-14",
188    backupTime: "02:00:00",
189    vpsRegion: "us-west-2",
190    status: "completed",
191    size: "856 MB",
192    duration: "5m 18s",
193    type: "full",
194    retentionDays: 30,
195    compression: true,
196    encryption: true,
197    nextScheduled: "2024-01-15 02:00:00",
198    logs: [
199      {
200        id: "log-10",
201        timestamp: "2024-01-14 02:00:00",
202        level: "info",
203        message: "Backup started",
204      },
205      {
206        id: "log-11",
207        timestamp: "2024-01-14 02:05:18",
208        level: "info",
209        message: "Backup completed successfully",
210      },
211    ],
212  },
213];
214
215function StatusBadge({ status }: { status: DatabaseBackup["status"] }) {
216  const variants = {
217    completed: {
218      variant: "default" as const,
219      icon: CheckCircle,
220      className: "bg-green-100 text-green-800 border-green-200",
221    },
222    failed: {
223      variant: "destructive" as const,
224      icon: XCircle,
225      className: "bg-red-100 text-red-800 border-red-200",
226    },
227    in_progress: {
228      variant: "secondary" as const,
229      icon: Activity,
230      className: "bg-blue-100 text-blue-800 border-blue-200",
231    },
232    pending: {
233      variant: "outline" as const,
234      icon: Clock,
235      className: "bg-yellow-100 text-yellow-800 border-yellow-200",
236    },
237  };
238
239  const config = variants[status];
240  const Icon = config.icon;
241
242  return (
243    <Badge
244      variant={config.variant}
245      className={cn("flex items-center gap-1", config.className)}
246    >
247      <Icon className="h-3 w-3" />
248      {status.replace("_", " ")}
249    </Badge>
250  );
251}
252
253function TypeBadge({ type }: { type: DatabaseBackup["type"] }) {
254  const variants = {
255    full: "bg-purple-100 text-purple-800 border-purple-200",
256    incremental: "bg-blue-100 text-blue-800 border-blue-200",
257    differential: "bg-orange-100 text-orange-800 border-orange-200",
258  };
259
260  return (
261    <Badge variant="outline" className={variants[type]}>
262      {type}
263    </Badge>
264  );
265}
266
267function LogLevelBadge({ level }: { level: BackupLog["level"] }) {
268  const variants = {
269    info: "bg-blue-100 text-blue-800 border-blue-200",
270    warning: "bg-yellow-100 text-yellow-800 border-yellow-200",
271    error: "bg-red-100 text-red-800 border-red-200",
272  };
273
274  return (
275    <Badge variant="outline" className={cn("text-xs", variants[level])}>
276      {level}
277    </Badge>
278  );
279}
280
281interface DatabaseBackupManagerProps {
282  backups?: DatabaseBackup[];
283}
284
285export function DatabaseBackupManager({
286  backups = mockBackups,
287}: DatabaseBackupManagerProps) {
288  const [searchTerm, setSearchTerm] = useState("");
289  const [statusFilter, setStatusFilter] = useState<string>("all");
290  const [regionFilter, setRegionFilter] = useState<string>("all");
291  const [selectedBackup, setSelectedBackup] = useState<DatabaseBackup | null>(
292    null
293  );
294  const [sortConfig, setSortConfig] = useState<{
295    key: keyof DatabaseBackup | null;
296    direction: "asc" | "desc";
297  }>({ key: null, direction: "asc" });
298
299  const filteredBackups = useMemo(() => {
300    return backups.filter((backup) => {
301      const matchesSearch =
302        backup.databaseName.toLowerCase().includes(searchTerm.toLowerCase()) ||
303        backup.vpsRegion.toLowerCase().includes(searchTerm.toLowerCase());
304      const matchesStatus =
305        statusFilter === "all" || backup.status === statusFilter;
306      const matchesRegion =
307        regionFilter === "all" || backup.vpsRegion === regionFilter;
308
309      return matchesSearch && matchesStatus && matchesRegion;
310    });
311  }, [backups, searchTerm, statusFilter, regionFilter]);
312
313  const sortedBackups = useMemo(() => {
314    if (!sortConfig.key) return filteredBackups;
315
316    return [...filteredBackups].sort((a, b) => {
317      const aValue = a[sortConfig.key!];
318      const bValue = b[sortConfig.key!];
319
320      // Handle undefined values
321      if (aValue === undefined && bValue === undefined) return 0;
322      if (aValue === undefined) return sortConfig.direction === "asc" ? 1 : -1;
323      if (bValue === undefined) return sortConfig.direction === "asc" ? -1 : 1;
324
325      if (aValue < bValue) {
326        return sortConfig.direction === "asc" ? -1 : 1;
327      }
328      if (aValue > bValue) {
329        return sortConfig.direction === "asc" ? 1 : -1;
330      }
331      return 0;
332    });
333  }, [filteredBackups, sortConfig]);
334
335  const handleSort = (key: keyof DatabaseBackup) => {
336    setSortConfig((current) => ({
337      key,
338      direction:
339        current.key === key && current.direction === "asc" ? "desc" : "asc",
340    }));
341  };
342
343  const uniqueRegions = Array.from(
344    new Set(backups.map((backup) => backup.vpsRegion))
345  );
346
347  const stats = {
348    total: backups.length,
349    completed: backups.filter((b) => b.status === "completed").length,
350    failed: backups.filter((b) => b.status === "failed").length,
351    inProgress: backups.filter((b) => b.status === "in_progress").length,
352    totalSize: backups.reduce((acc, backup) => {
353      const sizeValue = parseFloat(backup.size.split(" ")[0]);
354      const unit = backup.size.split(" ")[1];
355      if (unit === "GB") return acc + sizeValue * 1024;
356      if (unit === "MB") return acc + sizeValue;
357      return acc;
358    }, 0),
359  };
360
361  return (
362    <div className="w-full max-w-7xl mx-auto p-6 space-y-6">
363      {/* Header */}
364      <div className="flex flex-col gap-4">
365        <div className="flex items-center gap-2">
366          <Database className="h-8 w-8 text-primary" />
367          <div>
368            <h1 className="text-3xl font-bold">Database Backup Manager</h1>
369            <p className="text-muted-foreground">
370              Monitor and manage your database backups across all regions
371            </p>
372          </div>
373        </div>
374
375        {/* Stats Cards */}
376        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
377          <Card>
378            <CardContent className="p-4">
379              <div className="flex items-center gap-2">
380                <HardDrive className="h-5 w-5 text-blue-500" />
381                <div>
382                  <p className="text-sm text-muted-foreground">Total Backups</p>
383                  <p className="text-2xl font-bold">{stats.total}</p>
384                </div>
385              </div>
386            </CardContent>
387          </Card>
388          <Card>
389            <CardContent className="p-4">
390              <div className="flex items-center gap-2">
391                <CheckCircle className="h-5 w-5 text-green-500" />
392                <div>
393                  <p className="text-sm text-muted-foreground">Completed</p>
394                  <p className="text-2xl font-bold">{stats.completed}</p>
395                </div>
396              </div>
397            </CardContent>
398          </Card>
399          <Card>
400            <CardContent className="p-4">
401              <div className="flex items-center gap-2">
402                <XCircle className="h-5 w-5 text-red-500" />
403                <div>
404                  <p className="text-sm text-muted-foreground">Failed</p>
405                  <p className="text-2xl font-bold">{stats.failed}</p>
406                </div>
407              </div>
408            </CardContent>
409          </Card>
410          <Card>
411            <CardContent className="p-4">
412              <div className="flex items-center gap-2">
413                <Server className="h-5 w-5 text-purple-500" />
414                <div>
415                  <p className="text-sm text-muted-foreground">Total Size</p>
416                  <p className="text-2xl font-bold">
417                    {(stats.totalSize / 1024).toFixed(1)} GB
418                  </p>
419                </div>
420              </div>
421            </CardContent>
422          </Card>
423        </div>
424      </div>
425
426      {/* Filters */}
427      <Card>
428        <CardContent className="p-4">
429          <div className="flex flex-col sm:flex-row gap-4">
430            <div className="relative flex-1">
431              <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
432              <Input
433                placeholder="Search databases or regions..."
434                value={searchTerm}
435                onChange={(e) => setSearchTerm(e.target.value)}
436                className="pl-10"
437              />
438            </div>
439            <Select value={statusFilter} onValueChange={setStatusFilter}>
440              <SelectTrigger className="w-full sm:w-[180px]">
441                <SelectValue placeholder="Filter by status" />
442              </SelectTrigger>
443              <SelectContent>
444                <SelectItem value="all">All Statuses</SelectItem>
445                <SelectItem value="completed">Completed</SelectItem>
446                <SelectItem value="failed">Failed</SelectItem>
447                <SelectItem value="in_progress">In Progress</SelectItem>
448                <SelectItem value="pending">Pending</SelectItem>
449              </SelectContent>
450            </Select>
451            <Select value={regionFilter} onValueChange={setRegionFilter}>
452              <SelectTrigger className="w-full sm:w-[180px]">
453                <SelectValue placeholder="Filter by region" />
454              </SelectTrigger>
455              <SelectContent>
456                <SelectItem value="all">All Regions</SelectItem>
457                {uniqueRegions.map((region) => (
458                  <SelectItem key={region} value={region}>
459                    {region}
460                  </SelectItem>
461                ))}
462              </SelectContent>
463            </Select>
464          </div>
465        </CardContent>
466      </Card>
467
468      {/* Main Content */}
469      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
470        {/* Backup List */}
471        <div className="lg:col-span-2">
472          <Card>
473            <CardHeader>
474              <CardTitle>Backup History</CardTitle>
475              <CardDescription>
476                Recent database backup operations
477              </CardDescription>
478            </CardHeader>
479            <CardContent className="p-0">
480              <Table>
481                <TableHeader>
482                  <TableRow>
483                    <TableHead
484                      className="cursor-pointer hover:bg-muted/50"
485                      onClick={() => handleSort("databaseName")}
486                    >
487                      <div className="flex items-center gap-2">
488                        Database
489                        {sortConfig.key === "databaseName" &&
490                          (sortConfig.direction === "asc" ? (
491                            <ChevronUp className="h-4 w-4" />
492                          ) : (
493                            <ChevronDown className="h-4 w-4" />
494                          ))}
495                      </div>
496                    </TableHead>
497                    <TableHead
498                      className="cursor-pointer hover:bg-muted/50"
499                      onClick={() => handleSort("backupDate")}
500                    >
501                      <div className="flex items-center gap-2">
502                        Date/Time
503                        {sortConfig.key === "backupDate" &&
504                          (sortConfig.direction === "asc" ? (
505                            <ChevronUp className="h-4 w-4" />
506                          ) : (
507                            <ChevronDown className="h-4 w-4" />
508                          ))}
509                      </div>
510                    </TableHead>
511                    <TableHead>Status</TableHead>
512                    <TableHead>Region</TableHead>
513                    <TableHead>Size</TableHead>
514                    <TableHead>Actions</TableHead>
515                  </TableRow>
516                </TableHeader>
517                <TableBody>
518                  {sortedBackups.map((backup) => (
519                    <TableRow
520                      key={backup.id}
521                      className="cursor-pointer hover:bg-muted/50"
522                      onClick={() => setSelectedBackup(backup)}
523                    >
524                      <TableCell>
525                        <div className="flex flex-col">
526                          <span className="font-medium">
527                            {backup.databaseName}
528                          </span>
529                          <div className="flex items-center gap-2 mt-1">
530                            <TypeBadge type={backup.type} />
531                            {backup.encryption && (
532                              <Badge
533                                variant="outline"
534                                className="text-xs bg-green-50 text-green-700 border-green-200"
535                              >
536                                Encrypted
537                              </Badge>
538                            )}
539                          </div>
540                        </div>
541                      </TableCell>
542                      <TableCell>
543                        <div className="flex items-center gap-2">
544                          <Calendar className="h-4 w-4 text-muted-foreground" />
545                          <div className="flex flex-col">
546                            <span className="text-sm">{backup.backupDate}</span>
547                            <span className="text-xs text-muted-foreground">
548                              {backup.backupTime}
549                            </span>
550                          </div>
551                        </div>
552                      </TableCell>
553                      <TableCell>
554                        <StatusBadge status={backup.status} />
555                      </TableCell>
556                      <TableCell>
557                        <div className="flex items-center gap-2">
558                          <MapPin className="h-4 w-4 text-muted-foreground" />
559                          <span className="text-sm">{backup.vpsRegion}</span>
560                        </div>
561                      </TableCell>
562                      <TableCell>
563                        <div className="flex flex-col">
564                          <span className="text-sm font-medium">
565                            {backup.size}
566                          </span>
567                          <span className="text-xs text-muted-foreground">
568                            {backup.duration}
569                          </span>
570                        </div>
571                      </TableCell>
572                      <TableCell>
573                        <div className="flex items-center gap-1">
574                          <Button
575                            variant="ghost"
576                            size="sm"
577                            onClick={(e) => {
578                              e.stopPropagation();
579                              setSelectedBackup(backup);
580                            }}
581                          >
582                            <Eye className="h-4 w-4" />
583                          </Button>
584                          {backup.status === "completed" && (
585                            <Button variant="ghost" size="sm">
586                              <Download className="h-4 w-4" />
587                            </Button>
588                          )}
589                          {backup.status === "failed" && (
590                            <Button variant="ghost" size="sm">
591                              <RotateCcw className="h-4 w-4" />
592                            </Button>
593                          )}
594                        </div>
595                      </TableCell>
596                    </TableRow>
597                  ))}
598                </TableBody>
599              </Table>
600            </CardContent>
601          </Card>
602        </div>
603
604        {/* Backup Details */}
605        <div>
606          <Card>
607            <CardHeader>
608              <CardTitle>Backup Details</CardTitle>
609              <CardDescription>
610                {selectedBackup
611                  ? `Details for ${selectedBackup.databaseName}`
612                  : "Select a backup to view details"}
613              </CardDescription>
614            </CardHeader>
615            <CardContent>
616              {selectedBackup ? (
617                <Tabs defaultValue="overview" className="w-full">
618                  <TabsList className="grid w-full grid-cols-2">
619                    <TabsTrigger value="overview">Overview</TabsTrigger>
620                    <TabsTrigger value="logs">Logs</TabsTrigger>
621                  </TabsList>
622                  <TabsContent value="overview" className="space-y-4">
623                    <div className="space-y-3">
624                      <div className="flex justify-between">
625                        <span className="text-sm text-muted-foreground">
626                          Database:
627                        </span>
628                        <span className="text-sm font-medium">
629                          {selectedBackup.databaseName}
630                        </span>
631                      </div>
632                      <div className="flex justify-between">
633                        <span className="text-sm text-muted-foreground">
634                          Status:
635                        </span>
636                        <StatusBadge status={selectedBackup.status} />
637                      </div>
638                      <div className="flex justify-between">
639                        <span className="text-sm text-muted-foreground">
640                          Type:
641                        </span>
642                        <TypeBadge type={selectedBackup.type} />
643                      </div>
644                      <div className="flex justify-between">
645                        <span className="text-sm text-muted-foreground">
646                          Region:
647                        </span>
648                        <span className="text-sm">
649                          {selectedBackup.vpsRegion}
650                        </span>
651                      </div>
652                      <div className="flex justify-between">
653                        <span className="text-sm text-muted-foreground">
654                          Size:
655                        </span>
656                        <span className="text-sm font-medium">
657                          {selectedBackup.size}
658                        </span>
659                      </div>
660                      <div className="flex justify-between">
661                        <span className="text-sm text-muted-foreground">
662                          Duration:
663                        </span>
664                        <span className="text-sm">
665                          {selectedBackup.duration}
666                        </span>
667                      </div>
668                      <div className="flex justify-between">
669                        <span className="text-sm text-muted-foreground">
670                          Retention:
671                        </span>
672                        <span className="text-sm">
673                          {selectedBackup.retentionDays} days
674                        </span>
675                      </div>
676                      <div className="flex justify-between">
677                        <span className="text-sm text-muted-foreground">
678                          Compression:
679                        </span>
680                        <Badge
681                          variant={
682                            selectedBackup.compression ? "default" : "secondary"
683                          }
684                          className="text-xs"
685                        >
686                          {selectedBackup.compression ? "Enabled" : "Disabled"}
687                        </Badge>
688                      </div>
689                      <div className="flex justify-between">
690                        <span className="text-sm text-muted-foreground">
691                          Encryption:
692                        </span>
693                        <Badge
694                          variant={
695                            selectedBackup.encryption ? "default" : "secondary"
696                          }
697                          className="text-xs"
698                        >
699                          {selectedBackup.encryption ? "Enabled" : "Disabled"}
700                        </Badge>
701                      </div>
702                      {selectedBackup.nextScheduled && (
703                        <div className="flex justify-between">
704                          <span className="text-sm text-muted-foreground">
705                            Next Backup:
706                          </span>
707                          <span className="text-sm">
708                            {selectedBackup.nextScheduled}
709                          </span>
710                        </div>
711                      )}
712                    </div>
713                    {selectedBackup.status === "completed" && (
714                      <div className="pt-4 space-y-2">
715                        <Button className="w-full" size="sm">
716                          <Download className="h-4 w-4 mr-2" />
717                          Download Backup
718                        </Button>
719                        <Button variant="outline" className="w-full" size="sm">
720                          <RotateCcw className="h-4 w-4 mr-2" />
721                          Restore Database
722                        </Button>
723                      </div>
724                    )}
725                  </TabsContent>
726                  <TabsContent value="logs" className="space-y-3">
727                    <div className="max-h-64 overflow-y-auto space-y-2">
728                      {selectedBackup.logs.map((log) => (
729                        <div
730                          key={log.id}
731                          className="p-3 border rounded-lg bg-muted/30"
732                        >
733                          <div className="flex items-center justify-between mb-1">
734                            <LogLevelBadge level={log.level} />
735                            <span className="text-xs text-muted-foreground">
736                              {log.timestamp}
737                            </span>
738                          </div>
739                          <p className="text-sm">{log.message}</p>
740                        </div>
741                      ))}
742                    </div>
743                  </TabsContent>
744                </Tabs>
745              ) : (
746                <div className="text-center py-8">
747                  <Database className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
748                  <p className="text-sm text-muted-foreground">
749                    Select a backup from the list to view details
750                  </p>
751                </div>
752              )}
753            </CardContent>
754          </Card>
755        </div>
756      </div>
757    </div>
758  );
759}
760
761export default function Component() {
762  return <DatabaseBackupManager />;
763}

Dependencies

External Libraries

lucide-reactreact

Shadcn/UI Components

badgebuttoncardinputselecttabletabs

Local Components

/lib/utils

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.