├── App
├── .bolt
│ ├── config.json
│ ├── ignore
│ └── prompt
├── .eslintrc.json
├── .gitignore
├── app
│ ├── globals.css
│ ├── layout.tsx
│ ├── metadata.ts
│ └── page.tsx
├── components.json
├── components
│ ├── FileUploader.tsx
│ ├── FolderList.tsx
│ ├── ImageGrid.tsx
│ ├── ImageLibrary.tsx
│ ├── ImageMetadata.tsx
│ ├── Pagination.tsx
│ ├── SearchBar.tsx
│ ├── SocialLinks.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
├── hooks
│ ├── use-folder-store.ts
│ └── use-toast.ts
├── lib
│ ├── utils.ts
│ └── utils
│ │ └── metadata.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── tailwind.config.ts
└── tsconfig.json
├── LICENSE
├── README.md
└── Start.bat
/App/.bolt/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "nextjs-shadcn"
3 | }
4 |
--------------------------------------------------------------------------------
/App/.bolt/ignore:
--------------------------------------------------------------------------------
1 | components/ui/*
2 | hooks/use-toast.ts
3 |
--------------------------------------------------------------------------------
/App/.bolt/prompt:
--------------------------------------------------------------------------------
1 | For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
2 |
3 | When using client-side hooks (useState and useEffect) in a component that's being treated as a Server Component by Next.js, always add the "use client" directive at the top of the file.
4 |
5 | Do not write code that will trigger this error: "Warning: Extra attributes from the server: %s%s""class,style"
6 |
7 | By default, this template supports JSX syntax with Tailwind CSS classes, the shadcn/ui library, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
8 |
9 | Use icons from lucide-react for logos.
10 |
11 | Use stock photos from unsplash where appropriate, only valid URLs you know exist.
12 |
--------------------------------------------------------------------------------
/App/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/App/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/App/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | @layer base {
20 | :root {
21 | --background: 0 0% 100%;
22 | --foreground: 0 0% 3.9%;
23 | --card: 0 0% 100%;
24 | --card-foreground: 0 0% 3.9%;
25 | --popover: 0 0% 100%;
26 | --popover-foreground: 0 0% 3.9%;
27 | --primary: 0 0% 9%;
28 | --primary-foreground: 0 0% 98%;
29 | --secondary: 0 0% 96.1%;
30 | --secondary-foreground: 0 0% 9%;
31 | --muted: 0 0% 96.1%;
32 | --muted-foreground: 0 0% 45.1%;
33 | --accent: 0 0% 96.1%;
34 | --accent-foreground: 0 0% 9%;
35 | --destructive: 0 84.2% 60.2%;
36 | --destructive-foreground: 0 0% 98%;
37 | --border: 0 0% 89.8%;
38 | --input: 0 0% 89.8%;
39 | --ring: 0 0% 3.9%;
40 | --chart-1: 12 76% 61%;
41 | --chart-2: 173 58% 39%;
42 | --chart-3: 197 37% 24%;
43 | --chart-4: 43 74% 66%;
44 | --chart-5: 27 87% 67%;
45 | --radius: 0.5rem;
46 | }
47 | .dark {
48 | --background: 0 0% 3.9%;
49 | --foreground: 0 0% 98%;
50 | --card: 0 0% 3.9%;
51 | --card-foreground: 0 0% 98%;
52 | --popover: 0 0% 3.9%;
53 | --popover-foreground: 0 0% 98%;
54 | --primary: 0 0% 98%;
55 | --primary-foreground: 0 0% 9%;
56 | --secondary: 0 0% 14.9%;
57 | --secondary-foreground: 0 0% 98%;
58 | --muted: 0 0% 14.9%;
59 | --muted-foreground: 0 0% 63.9%;
60 | --accent: 0 0% 14.9%;
61 | --accent-foreground: 0 0% 98%;
62 | --destructive: 0 62.8% 30.6%;
63 | --destructive-foreground: 0 0% 98%;
64 | --border: 0 0% 14.9%;
65 | --input: 0 0% 14.9%;
66 | --ring: 0 0% 83.1%;
67 | --chart-1: 220 70% 50%;
68 | --chart-2: 160 60% 45%;
69 | --chart-3: 30 80% 55%;
70 | --chart-4: 280 65% 60%;
71 | --chart-5: 340 75% 55%;
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/App/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 | import { Inter } from 'next/font/google';
3 | import { ThemeProvider } from "next-themes";
4 | import { metadata } from './metadata';
5 |
6 | const inter = Inter({ subsets: ['latin'] });
7 |
8 | export { metadata };
9 |
10 | export default function RootLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode;
14 | }) {
15 | return (
16 |
17 |
18 |
24 | {children}
25 |
26 |
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/App/app/metadata.ts:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | export const metadata: Metadata = {
4 | title: 'ComfyUI Image Library',
5 | description: 'A library for ComfyUI generated images',
6 | };
--------------------------------------------------------------------------------
/App/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from 'next/dynamic';
4 |
5 | const ImageLibrary = dynamic(() => import('@/components/ImageLibrary'), {
6 | ssr: false
7 | });
8 |
9 | export default function Home() {
10 | return ;
11 | }
--------------------------------------------------------------------------------
/App/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/App/components/FileUploader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FolderOpen } from "lucide-react";
4 | import { Button } from "@/components/ui/button";
5 |
6 | interface FileUploaderProps {
7 | onFolderSelect: (folder: any) => void;
8 | }
9 |
10 | export function FileUploader({ onFolderSelect }: FileUploaderProps) {
11 | const handleFolderSelect = async () => {
12 | try {
13 | if (typeof window === 'undefined') return;
14 |
15 | // Check if the API is available
16 | if (!('showDirectoryPicker' in window)) {
17 | throw new Error('File System Access API is not supported in this environment');
18 | }
19 |
20 | // @ts-ignore - FileSystemDirectoryHandle is experimental
21 | const dirHandle = await window.showDirectoryPicker({
22 | mode: 'read'
23 | });
24 | onFolderSelect(dirHandle);
25 | } catch (err) {
26 | console.error("Error selecting folder:", err);
27 | // Handle the error gracefully
28 | if (err instanceof Error && err.name === 'SecurityError') {
29 | alert('Unable to access files. This feature may not be available in the current environment.');
30 | }
31 | }
32 | };
33 |
34 | return (
35 |
42 | );
43 | }
--------------------------------------------------------------------------------
/App/components/FolderList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ScrollArea } from "@/components/ui/scroll-area";
4 | import { Button } from "@/components/ui/button";
5 | import { Folder, Trash2 } from "lucide-react";
6 | import { cn } from "@/lib/utils";
7 |
8 | interface FolderListProps {
9 | folders: Array<{ name: string; path: string }>;
10 | currentFolder: string | null;
11 | onFolderSelect: (path: string) => void;
12 | onFolderDelete: (path: string) => void;
13 | }
14 |
15 | export function FolderList({
16 | folders,
17 | currentFolder,
18 | onFolderSelect,
19 | onFolderDelete
20 | }: FolderListProps) {
21 | return (
22 |
23 |
24 | {folders.map((folder) => (
25 |
26 |
37 |
45 |
46 | ))}
47 |
48 |
49 | );
50 | }
--------------------------------------------------------------------------------
/App/components/ImageGrid.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AspectRatio } from "@/components/ui/aspect-ratio";
4 | import { ScrollArea } from "@/components/ui/scroll-area";
5 | import Image from "next/image";
6 | import { useState } from "react";
7 | import { Skeleton } from "@/components/ui/skeleton";
8 |
9 | interface ImageGridProps {
10 | images: Array<{
11 | path: string;
12 | name: string;
13 | }>;
14 | onImageSelect: (image: any) => void;
15 | }
16 |
17 | export function ImageGrid({ images, onImageSelect }: ImageGridProps) {
18 | const [loadingStates, setLoadingStates] = useState<{ [key: string]: boolean }>({});
19 | const [errorStates, setErrorStates] = useState<{ [key: string]: boolean }>({});
20 |
21 | const handleImageLoad = (imagePath: string) => {
22 | setLoadingStates(prev => ({ ...prev, [imagePath]: false }));
23 | };
24 |
25 | const handleImageError = (imagePath: string) => {
26 | setLoadingStates(prev => ({ ...prev, [imagePath]: false }));
27 | setErrorStates(prev => ({ ...prev, [imagePath]: true }));
28 | };
29 |
30 | return (
31 |
32 |
33 | {images.map((image, index) => (
34 |
!errorStates[image.path] && onImageSelect(image)}
38 | >
39 |
40 | {!errorStates[image.path] ? (
41 | <>
42 | {(loadingStates[image.path] !== false) && (
43 |
44 | )}
45 | handleImageLoad(image.path)}
53 | onError={() => handleImageError(image.path)}
54 | />
55 | >
56 | ) : (
57 |
60 | )}
61 |
62 |
63 | ))}
64 |
65 |
66 | );
67 | }
--------------------------------------------------------------------------------
/App/components/ImageLibrary.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from 'react';
4 | import { FolderList } from './FolderList';
5 | import { ImageGrid } from './ImageGrid';
6 | import { ImageMetadata } from './ImageMetadata';
7 | import { SocialLinks } from './SocialLinks';
8 | import { Button } from '@/components/ui/button';
9 | import { Input } from '@/components/ui/input';
10 | import { FolderOpen, Search, SortAsc, SortDesc } from 'lucide-react';
11 | import { useFolderStore } from '@/hooks/use-folder-store';
12 | import { readImageMetadata } from '@/lib/utils';
13 | import { Pagination } from './Pagination';
14 |
15 | const IMAGES_PER_PAGE = 50;
16 |
17 | export default function ImageLibrary() {
18 | const [currentFolder, setCurrentFolder] = useState(null);
19 | const [images, setImages] = useState([]);
20 | const [selectedImage, setSelectedImage] = useState(null);
21 | const [searchQuery, setSearchQuery] = useState('');
22 | const [sortAscending, setSortAscending] = useState(false);
23 | const [currentPage, setCurrentPage] = useState(1);
24 | const [isLoading, setIsLoading] = useState(false);
25 | const {
26 | folders,
27 | addFolder,
28 | removeFolder,
29 | updateFolderHandle,
30 | updateLastAccessed,
31 | getLastAccessedFolder
32 | } = useFolderStore();
33 |
34 | const handleFolderSelect = async (folderHandle: FileSystemDirectoryHandle) => {
35 | try {
36 | setIsLoading(true);
37 | const images = [];
38 | for await (const entry of folderHandle.values()) {
39 | if (entry.kind === 'file' && entry.name.toLowerCase().endsWith('.png')) {
40 | const metadata = await readImageMetadata(entry);
41 | const file = await entry.getFile();
42 | images.push({
43 | path: URL.createObjectURL(file),
44 | metadata,
45 | name: entry.name,
46 | lastModified: file.lastModified,
47 | });
48 | }
49 | }
50 | setImages(images);
51 | setCurrentPage(1);
52 | } catch (error) {
53 | console.error('Error reading folder:', error);
54 | } finally {
55 | setIsLoading(false);
56 | }
57 | };
58 |
59 | const openFolder = async () => {
60 | try {
61 | const handle = await window.showDirectoryPicker();
62 | const folder = {
63 | name: handle.name,
64 | path: handle.name,
65 | handle,
66 | };
67 | addFolder(folder);
68 | setCurrentFolder(folder.path);
69 | await handleFolderSelect(handle);
70 | } catch (error) {
71 | console.error('Error selecting folder:', error);
72 | }
73 | };
74 |
75 | const handleFolderDelete = (path: string) => {
76 | if (currentFolder === path) {
77 | setImages([]);
78 | setCurrentFolder(null);
79 | }
80 | removeFolder(path);
81 | };
82 |
83 | const handleStoredFolderSelect = async (path: string) => {
84 | try {
85 | const folder = folders.find((f) => f.path === path);
86 | if (!folder) return;
87 |
88 | setCurrentFolder(path);
89 | updateLastAccessed(path);
90 | let handle = folder.handle;
91 |
92 | if (!handle) {
93 | try {
94 | handle = await window.showDirectoryPicker({
95 | id: path,
96 | startIn: 'desktop',
97 | });
98 | updateFolderHandle(path, handle);
99 | } catch (error) {
100 | console.error('Error getting folder handle:', error);
101 | return;
102 | }
103 | }
104 |
105 | // Verify permission
106 | try {
107 | await handle.requestPermission({ mode: 'read' });
108 | await handleFolderSelect(handle);
109 | } catch (error) {
110 | console.error('Permission denied, requesting new handle:', error);
111 | try {
112 | handle = await window.showDirectoryPicker({
113 | id: path,
114 | startIn: 'desktop',
115 | });
116 | updateFolderHandle(path, handle);
117 | await handleFolderSelect(handle);
118 | } catch (innerError) {
119 | console.error('Error selecting folder:', innerError);
120 | }
121 | }
122 | } catch (error) {
123 | console.error('Error accessing folder:', error);
124 | }
125 | };
126 |
127 | // Auto-load last accessed folder on mount
128 | useEffect(() => {
129 | const loadLastFolder = async () => {
130 | const lastFolder = getLastAccessedFolder();
131 | if (lastFolder?.path) {
132 | await handleStoredFolderSelect(lastFolder.path);
133 | }
134 | };
135 | loadLastFolder();
136 | }, []);
137 |
138 | const filteredAndSortedImages = images
139 | .filter((image) => {
140 | if (!searchQuery) return true;
141 | const query = searchQuery.toLowerCase();
142 | return (
143 | image.metadata?.toLowerCase().includes(query) ||
144 | image.name.toLowerCase().includes(query)
145 | );
146 | })
147 | .sort((a, b) => {
148 | const modifier = sortAscending ? 1 : -1;
149 | return (a.lastModified - b.lastModified) * modifier;
150 | });
151 |
152 | const totalPages = Math.ceil(filteredAndSortedImages.length / IMAGES_PER_PAGE);
153 | const paginatedImages = filteredAndSortedImages.slice(
154 | (currentPage - 1) * IMAGES_PER_PAGE,
155 | currentPage * IMAGES_PER_PAGE
156 | );
157 |
158 | useEffect(() => {
159 | // Reset to first page when search query changes
160 | setCurrentPage(1);
161 | }, [searchQuery]);
162 |
163 | return (
164 |
165 |
182 |
183 |
184 |
185 |
186 |
187 | setSearchQuery(e.target.value)}
192 | />
193 |
194 |
195 |
208 |
209 |
210 |
211 |
212 |
216 |
217 | {totalPages > 1 && (
218 |
223 | )}
224 |
225 | {selectedImage && (
226 |
setSelectedImage(null)}
229 | />
230 | )}
231 |
232 | );
233 | }
--------------------------------------------------------------------------------
/App/components/ImageMetadata.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { ScrollArea } from "@/components/ui/scroll-area";
5 | import { Copy, FileImage, X } from "lucide-react";
6 | import Image from "next/image";
7 | import { useEffect, useState } from "react";
8 |
9 | interface ImageMetadataProps {
10 | image: {
11 | path: string;
12 | name: string;
13 | metadata: any;
14 | };
15 | onClose: () => void;
16 | }
17 |
18 | interface ImageDimensions {
19 | width: number;
20 | height: number;
21 | }
22 |
23 | export function ImageMetadata({ image, onClose }: ImageMetadataProps) {
24 | const [imageDimensions, setImageDimensions] = useState(null);
25 |
26 | useEffect(() => {
27 | const img = new window.Image();
28 | img.onload = () => {
29 | setImageDimensions({
30 | width: img.naturalWidth,
31 | height: img.naturalHeight
32 | });
33 | };
34 | img.src = image.path;
35 | }, [image.path]);
36 |
37 | const copyToClipboard = (text: string) => {
38 | navigator.clipboard.writeText(text);
39 | };
40 |
41 | const parseMetadata = (metadata: any) => {
42 | try {
43 | if (!metadata) return null;
44 |
45 | const data = typeof metadata === 'string' ?
46 | JSON.parse(metadata.trim()) :
47 | metadata;
48 |
49 | // Find prompt from different possible locations
50 | let prompt = "";
51 |
52 | // Check for direct text input (new format)
53 | if (data["113"]?.inputs?.text) {
54 | prompt = data["113"].inputs.text;
55 | } else {
56 | // Find prompt node (CLIPTextEncode with non-empty text)
57 | const promptNode = Object.entries(data).find(([, value]: [string, any]) =>
58 | value?.class_type === "CLIPTextEncode" &&
59 | value?.inputs?.text &&
60 | !value?.inputs?.text?.includes("negative") &&
61 | value?.inputs?.text?.length > 0
62 | );
63 | if (promptNode) {
64 | prompt = promptNode[1].inputs.text;
65 | }
66 | }
67 |
68 | // Find model node
69 | const modelNode = Object.entries(data).find(([, value]: [string, any]) =>
70 | value?.class_type === "UnetLoaderGGUF" ||
71 | value?.class_type === "CheckpointLoaderSimple" ||
72 | value?.class_type === "UNETLoader"
73 | );
74 |
75 | // Find LoRA nodes
76 | const loraNames = [];
77 | Object.entries(data).forEach(([, value]: [string, any]) => {
78 | if (value?.class_type === "Power Lora Loader (rgthree)") {
79 | Object.entries(value.inputs).forEach(([key, val]: [string, any]) => {
80 | if (key.startsWith('lora_') && val?.on && val?.lora) {
81 | loraNames.push(val.lora);
82 | }
83 | });
84 | } else if (value?.class_type === "LoraLoader|pysssss") {
85 | if (value.inputs?.lora_name?.content) {
86 | loraNames.push(value.inputs.lora_name.content);
87 | }
88 | } else if (value?.inputs?.lora_name && typeof value.inputs.lora_name === 'string') {
89 | loraNames.push(value.inputs.lora_name);
90 | }
91 | });
92 |
93 | // Find seed
94 | const samplerNode = Object.entries(data).find(([, value]: [string, any]) =>
95 | value?.class_type === "KSampler" &&
96 | value?.inputs?.seed
97 | );
98 |
99 | return {
100 | prompt,
101 | modelName:
102 | modelNode?.[1]?.inputs?.unet_name ||
103 | modelNode?.[1]?.inputs?.ckpt_name ||
104 | "",
105 | loraNames: [...new Set(loraNames)], // Remove duplicates
106 | seed: samplerNode?.[1]?.inputs?.seed || ""
107 | };
108 | } catch (error) {
109 | console.error('Error parsing metadata:', error);
110 | return null;
111 | }
112 | };
113 |
114 | const metadata = parseMetadata(image.metadata);
115 |
116 | return (
117 |
118 |
119 |
Image Details
120 |
123 |
124 |
125 |
126 |
127 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | {image.name}
140 |
141 | {imageDimensions && (
142 |
143 | {imageDimensions.width} × {imageDimensions.height}
144 |
145 | )}
146 |
147 |
148 | {metadata ? (
149 | <>
150 |
151 |
152 |
Prompt
153 | {metadata.prompt && (
154 |
161 | )}
162 |
163 |
164 | {metadata.prompt || "No prompt available"}
165 |
166 |
167 |
168 |
169 |
Model
170 |
171 | {metadata.modelName || "No model information available"}
172 |
173 |
174 |
175 |
176 |
LoRA Models
177 |
178 | {metadata.loraNames?.length > 0 ? (
179 | metadata.loraNames.map((lora, index) => (
180 |
181 | {lora}
182 |
183 | ))
184 | ) : (
185 |
No LoRA models used
186 | )}
187 |
188 |
189 |
190 |
191 |
Seed
192 |
193 | {metadata.seed || "Seed not available"}
194 |
195 |
196 | >
197 | ) : (
198 |
No metadata available for this image
199 | )}
200 |
201 |
202 |
203 |
204 | );
205 | }
--------------------------------------------------------------------------------
/App/components/Pagination.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { ChevronLeft, ChevronRight } from "lucide-react";
5 |
6 | interface PaginationProps {
7 | currentPage: number;
8 | totalPages: number;
9 | onPageChange: (page: number) => void;
10 | }
11 |
12 | export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
13 | return (
14 |
15 |
16 |
25 |
26 | Page {currentPage} of {totalPages}
27 |
28 |
37 |
38 |
39 | );
40 | }
--------------------------------------------------------------------------------
/App/components/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Search } from "lucide-react";
4 | import { Input } from "@/components/ui/input";
5 |
6 | interface SearchBarProps {
7 | value: string;
8 | onChange: (value: string) => void;
9 | }
10 |
11 | export function SearchBar({ value, onChange }: SearchBarProps) {
12 | return (
13 |
14 |
15 |
16 | onChange(e.target.value)}
21 | className="flex-1"
22 | />
23 |
24 |
25 | );
26 | }
--------------------------------------------------------------------------------
/App/components/SocialLinks.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Instagram, Youtube, Globe, Twitter } from "lucide-react";
5 |
6 | export function SocialLinks() {
7 | return (
8 |
9 |
10 | ComfyGallery made by PixelAILabs.com. For more information, visit the links below.
11 |
12 |
13 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/App/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion';
5 | import { ChevronDown } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = 'AccordionItem';
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180',
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
--------------------------------------------------------------------------------
/App/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
5 |
6 | import { cn } from '@/lib/utils';
7 | import { buttonVariants } from '@/components/ui/button';
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root;
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal;
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ));
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | );
60 | AlertDialogHeader.displayName = 'AlertDialogHeader';
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | );
74 | AlertDialogFooter.displayName = 'AlertDialogFooter';
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ));
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ));
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName;
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ));
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | };
142 |
--------------------------------------------------------------------------------
/App/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const alertVariants = cva(
7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | destructive:
13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
14 | },
15 | },
16 | defaultVariants: {
17 | variant: 'default',
18 | },
19 | }
20 | );
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ));
33 | Alert.displayName = 'Alert';
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ));
45 | AlertTitle.displayName = 'AlertTitle';
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | AlertDescription.displayName = 'AlertDescription';
58 |
59 | export { Alert, AlertTitle, AlertDescription };
60 |
--------------------------------------------------------------------------------
/App/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root;
6 |
7 | export { AspectRatio };
8 |
--------------------------------------------------------------------------------
/App/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/App/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/App/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { ChevronRight, MoreHorizontal } from 'lucide-react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<'nav'> & {
10 | separator?: React.ReactNode;
11 | }
12 | >(({ ...props }, ref) => );
13 | Breadcrumb.displayName = 'Breadcrumb';
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<'ol'>
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | BreadcrumbList.displayName = 'BreadcrumbList';
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<'li'>
33 | >(({ className, ...props }, ref) => (
34 |
39 | ));
40 | BreadcrumbItem.displayName = 'BreadcrumbItem';
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<'a'> & {
45 | asChild?: boolean;
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : 'a';
49 |
50 | return (
51 |
56 | );
57 | });
58 | BreadcrumbLink.displayName = 'BreadcrumbLink';
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<'span'>
63 | >(({ className, ...props }, ref) => (
64 |
72 | ));
73 | BreadcrumbPage.displayName = 'BreadcrumbPage';
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<'li'>) => (
80 | svg]:size-3.5', className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | );
89 | BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<'span'>) => (
95 |
101 |
102 | More
103 |
104 | );
105 | BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | };
116 |
--------------------------------------------------------------------------------
/App/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | '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',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button';
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = 'Button';
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/App/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ChevronLeft, ChevronRight } from 'lucide-react';
5 | import { DayPicker } from 'react-day-picker';
6 |
7 | import { cn } from '@/lib/utils';
8 | import { buttonVariants } from '@/components/ui/button';
9 |
10 | export type CalendarProps = React.ComponentProps;
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
58 | IconRight: ({ ...props }) => ,
59 | }}
60 | {...props}
61 | />
62 | );
63 | }
64 | Calendar.displayName = 'Calendar';
65 |
66 | export { Calendar };
67 |
--------------------------------------------------------------------------------
/App/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = 'Card';
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = 'CardHeader';
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = 'CardTitle';
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = 'CardDescription';
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = 'CardContent';
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = 'CardFooter';
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/App/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from 'embla-carousel-react';
7 | import { ArrowLeft, ArrowRight } from 'lucide-react';
8 |
9 | import { cn } from '@/lib/utils';
10 | import { Button } from '@/components/ui/button';
11 |
12 | type CarouselApi = UseEmblaCarouselType[1];
13 | type UseCarouselParameters = Parameters;
14 | type CarouselOptions = UseCarouselParameters[0];
15 | type CarouselPlugin = UseCarouselParameters[1];
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions;
19 | plugins?: CarouselPlugin;
20 | orientation?: 'horizontal' | 'vertical';
21 | setApi?: (api: CarouselApi) => void;
22 | };
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0];
26 | api: ReturnType[1];
27 | scrollPrev: () => void;
28 | scrollNext: () => void;
29 | canScrollPrev: boolean;
30 | canScrollNext: boolean;
31 | } & CarouselProps;
32 |
33 | const CarouselContext = React.createContext(null);
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext);
37 |
38 | if (!context) {
39 | throw new Error('useCarousel must be used within a ');
40 | }
41 |
42 | return context;
43 | }
44 |
45 | const Carousel = React.forwardRef<
46 | HTMLDivElement,
47 | React.HTMLAttributes & CarouselProps
48 | >(
49 | (
50 | {
51 | orientation = 'horizontal',
52 | opts,
53 | setApi,
54 | plugins,
55 | className,
56 | children,
57 | ...props
58 | },
59 | ref
60 | ) => {
61 | const [carouselRef, api] = useEmblaCarousel(
62 | {
63 | ...opts,
64 | axis: orientation === 'horizontal' ? 'x' : 'y',
65 | },
66 | plugins
67 | );
68 | const [canScrollPrev, setCanScrollPrev] = React.useState(false);
69 | const [canScrollNext, setCanScrollNext] = React.useState(false);
70 |
71 | const onSelect = React.useCallback((api: CarouselApi) => {
72 | if (!api) {
73 | return;
74 | }
75 |
76 | setCanScrollPrev(api.canScrollPrev());
77 | setCanScrollNext(api.canScrollNext());
78 | }, []);
79 |
80 | const scrollPrev = React.useCallback(() => {
81 | api?.scrollPrev();
82 | }, [api]);
83 |
84 | const scrollNext = React.useCallback(() => {
85 | api?.scrollNext();
86 | }, [api]);
87 |
88 | const handleKeyDown = React.useCallback(
89 | (event: React.KeyboardEvent) => {
90 | if (event.key === 'ArrowLeft') {
91 | event.preventDefault();
92 | scrollPrev();
93 | } else if (event.key === 'ArrowRight') {
94 | event.preventDefault();
95 | scrollNext();
96 | }
97 | },
98 | [scrollPrev, scrollNext]
99 | );
100 |
101 | React.useEffect(() => {
102 | if (!api || !setApi) {
103 | return;
104 | }
105 |
106 | setApi(api);
107 | }, [api, setApi]);
108 |
109 | React.useEffect(() => {
110 | if (!api) {
111 | return;
112 | }
113 |
114 | onSelect(api);
115 | api.on('reInit', onSelect);
116 | api.on('select', onSelect);
117 |
118 | return () => {
119 | api?.off('select', onSelect);
120 | };
121 | }, [api, onSelect]);
122 |
123 | return (
124 |
137 |
145 | {children}
146 |
147 |
148 | );
149 | }
150 | );
151 | Carousel.displayName = 'Carousel';
152 |
153 | const CarouselContent = React.forwardRef<
154 | HTMLDivElement,
155 | React.HTMLAttributes
156 | >(({ className, ...props }, ref) => {
157 | const { carouselRef, orientation } = useCarousel();
158 |
159 | return (
160 |
171 | );
172 | });
173 | CarouselContent.displayName = 'CarouselContent';
174 |
175 | const CarouselItem = React.forwardRef<
176 | HTMLDivElement,
177 | React.HTMLAttributes
178 | >(({ className, ...props }, ref) => {
179 | const { orientation } = useCarousel();
180 |
181 | return (
182 |
193 | );
194 | });
195 | CarouselItem.displayName = 'CarouselItem';
196 |
197 | const CarouselPrevious = React.forwardRef<
198 | HTMLButtonElement,
199 | React.ComponentProps
200 | >(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
201 | const { orientation, scrollPrev, canScrollPrev } = useCarousel();
202 |
203 | return (
204 |
222 | );
223 | });
224 | CarouselPrevious.displayName = 'CarouselPrevious';
225 |
226 | const CarouselNext = React.forwardRef<
227 | HTMLButtonElement,
228 | React.ComponentProps
229 | >(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
230 | const { orientation, scrollNext, canScrollNext } = useCarousel();
231 |
232 | return (
233 |
251 | );
252 | });
253 | CarouselNext.displayName = 'CarouselNext';
254 |
255 | export {
256 | type CarouselApi,
257 | Carousel,
258 | CarouselContent,
259 | CarouselItem,
260 | CarouselPrevious,
261 | CarouselNext,
262 | };
263 |
--------------------------------------------------------------------------------
/App/components/ui/chart.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as RechartsPrimitive from 'recharts';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | // Format: { THEME_NAME: CSS_SELECTOR }
9 | const THEMES = { light: '', dark: '.dark' } as const;
10 |
11 | export type ChartConfig = {
12 | [k in string]: {
13 | label?: React.ReactNode;
14 | icon?: React.ComponentType;
15 | } & (
16 | | { color?: string; theme?: never }
17 | | { color?: never; theme: Record }
18 | );
19 | };
20 |
21 | type ChartContextProps = {
22 | config: ChartConfig;
23 | };
24 |
25 | const ChartContext = React.createContext(null);
26 |
27 | function useChart() {
28 | const context = React.useContext(ChartContext);
29 |
30 | if (!context) {
31 | throw new Error('useChart must be used within a ');
32 | }
33 |
34 | return context;
35 | }
36 |
37 | const ChartContainer = React.forwardRef<
38 | HTMLDivElement,
39 | React.ComponentProps<'div'> & {
40 | config: ChartConfig;
41 | children: React.ComponentProps<
42 | typeof RechartsPrimitive.ResponsiveContainer
43 | >['children'];
44 | }
45 | >(({ id, className, children, config, ...props }, ref) => {
46 | const uniqueId = React.useId();
47 | const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
48 |
49 | return (
50 |
51 |
60 |
61 |
62 | {children}
63 |
64 |
65 |
66 | );
67 | });
68 | ChartContainer.displayName = 'Chart';
69 |
70 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71 | const colorConfig = Object.entries(config).filter(
72 | ([_, config]) => config.theme || config.color
73 | );
74 |
75 | if (!colorConfig.length) {
76 | return null;
77 | }
78 |
79 | return (
80 |