aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authoromagdy7 <omar.professional8777@gmail.com>2024-05-15 23:23:57 +0300
committeromagdy7 <omar.professional8777@gmail.com>2024-05-15 23:23:57 +0300
commitcf857bc8af5ac3725f3bdb40dcdc80752595652f (patch)
tree72545ee4f47e133af811b8fb37db405e9b624c1d /frontend/src
parent1aa678533f0d21f8696754b1a1f456827f249b1c (diff)
downloadcloudrender-cf857bc8af5ac3725f3bdb40dcdc80752595652f.tar.xz
cloudrender-cf857bc8af5ac3725f3bdb40dcdc80752595652f.zip
Final version of backend and frontend
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.tsx2
-rw-r--r--frontend/src/components/Dashboard.tsx186
-rw-r--r--frontend/src/components/ImageProcessor.tsx67
-rw-r--r--frontend/src/components/ImageSideBar.tsx24
-rw-r--r--frontend/src/components/ui/button.tsx56
-rw-r--r--frontend/src/components/ui/input.tsx25
-rw-r--r--frontend/src/components/ui/select.tsx158
-rw-r--r--frontend/src/index.css77
-rw-r--r--frontend/src/lib/utils.ts6
9 files changed, 576 insertions, 25 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 370832d..cebfe44 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,9 +1,11 @@
import './App.css'
+import Dashboard from './components/Dashboard'
import ImageProcessor from './components/ImageProcessor'
function App() {
return (
+ // <Dashboard clusterName={"cloudrender-backend-cluster"} />
<ImageProcessor />
)
}
diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx
new file mode 100644
index 0000000..0834bee
--- /dev/null
+++ b/frontend/src/components/Dashboard.tsx
@@ -0,0 +1,186 @@
+import { useEffect, useState } from 'react';
+import { EKSClient, DescribeClusterCommand } from '@aws-sdk/client-eks';
+import axios from 'axios';
+
+// Configure AWS SDK v3
+const region = 'us-east-1'; // Change to your region
+
+
+// Create an Axios instance
+const axiosInstance = axios.create();
+
+// Add a request interceptor
+axiosInstance.interceptors.request.use(config => {
+ config.headers['Access-Control-Allow-Origin'] = '*';
+ return config;
+}, error => {
+ return Promise.reject(error);
+});
+
+
+
+const eksClient = new EKSClient({
+ region,
+ credentials: {
+ accessKeyId: "AKIA3X4DCJJWHYF5M42F",
+ secretAccessKey: "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",
+ },
+});
+
+const Header = () => {
+ return (
+ <header className="bg-gray-900 text-white py-4 px-6">
+ <h1 className="text-2xl font-bold">Kubernetes Dashboard</h1>
+ </header>
+ );
+};
+
+const Card = ({ title, children, className = "" }) => {
+ return (
+ <div className={`bg-white dark:bg-gray-900 rounded-lg shadow-md p-6 ${className}`}>
+ <h2 className="text-lg font-bold mb-4 dark:text-white">{title}</h2>
+ {children}
+ </div>
+ );
+};
+
+const StatusItem = ({ value, label }) => {
+ return (
+ <div className="bg-gray-200 dark:bg-gray-800 rounded-lg p-4">
+ <p className="text-4xl font-bold dark:text-white">{value}</p>
+ <p className="text-gray-500 dark:text-gray-400">{label}</p>
+ </div>
+ );
+};
+
+const StatusTable = ({ headers, rows }) => {
+ return (
+ <div className="overflow-x-auto">
+ <table className="w-full table-auto">
+ <thead>
+ <tr className="bg-gray-200 dark:bg-gray-800">
+ {headers.map((header, index) => (
+ <th key={index} className="px-4 py-2 text-left dark:text-white">{header}</th>
+ ))}
+ </tr>
+ </thead>
+ <tbody>
+ {rows.map((row, rowIndex) => (
+ <tr key={rowIndex} className="border-b border-gray-200 dark:border-gray-800">
+ {row.map((cell, cellIndex) => (
+ <td key={cellIndex} className={`px-4 py-2 ${cellIndex >= headers.length - 2 ? 'text-right' : 'text-left'} dark:text-white`}>
+ {cell}
+ </td>
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
+};
+
+const Dashboard = ({ clusterName }) => {
+ const [clusterOverview, setClusterOverview] = useState({});
+ const [nodes, setNodes] = useState([]);
+ const [pods, setPods] = useState([]);
+
+ useEffect(() => {
+ const fetchClusterData = async () => {
+ try {
+ const clusterData = await eksClient.send(new DescribeClusterCommand({ name: clusterName }));
+ const endpoint = clusterData.cluster.endpoint;
+
+ console.log(endpoint)
+
+ const nodeCount = await getNodeCount(endpoint);
+ const podCount = await getPodCount(endpoint);
+ const serviceCount = await getServiceCount(endpoint);
+
+ setClusterOverview({
+ nodes: nodeCount,
+ pods: podCount,
+ services: serviceCount,
+ });
+
+ const nodesData = await getNodeStatuses(endpoint);
+ setNodes(nodesData);
+
+ const podsData = await getPodStatuses(endpoint);
+ setPods(podsData);
+
+ } catch (error) {
+ console.error('Error fetching data from AWS:', error);
+ }
+ };
+
+ fetchClusterData();
+ }, [clusterName]);
+
+ const getNodeCount = async (endpoint) => {
+ const response = await axiosInstance.get(`${endpoint}/api/v1/nodes`);
+ return response.data.items.length;
+ };
+
+ const getPodCount = async (endpoint) => {
+ const response = await axiosInstance.get(`${endpoint}/api/v1/pods`);
+ return response.data.items.length;
+ };
+
+ const getServiceCount = async (endpoint) => {
+ const response = await axiosInstance.get(`${endpoint}/api/v1/services`);
+ return response.data.items.length;
+ };
+
+ const getNodeStatuses = async (endpoint) => {
+ const response = await axiosInstance.get(`${endpoint}/api/v1/nodes`);
+ return response.data.items.map(node => ({
+ name: node.metadata.name,
+ status: node.status.conditions.find(condition => condition.type === 'Ready').status === 'True' ? 'Running' : 'NotReady',
+ cpu: `${Math.round((node.status.allocatable.cpu / node.status.capacity.cpu) * 100)}%`,
+ memory: `${Math.round((node.status.allocatable.memory / node.status.capacity.memory) * 100)}%`,
+ }));
+ };
+
+ const getPodStatuses = async (endpoint) => {
+ const response = await axiosInstance.get(`${endpoint}/api/v1/pods`);
+ return response.data.items.map(pod => ({
+ name: pod.metadata.name,
+ status: pod.status.phase,
+ image: pod.spec.containers[0].image,
+ }));
+ };
+
+ return (
+ <div className="flex flex-col h-screen dark:bg-gray-900">
+ <Header />
+ <main className="flex-1 bg-gray-100 dark:bg-gray-800 p-6">
+ <div className="grid grid-cols-1 gap-6">
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
+ <Card title="Cluster Overview">
+ <div className="grid grid-cols-3 gap-4">
+ <StatusItem value={clusterOverview.nodes} label="Nodes" />
+ <StatusItem value={clusterOverview.pods} label="Pods" />
+ <StatusItem value={clusterOverview.services} label="Services" />
+ </div>
+ </Card>
+ <Card title="Node Status" className="md:col-span-2">
+ <StatusTable
+ headers={['Name', 'Status', 'CPU', 'Memory']}
+ rows={nodes.map(node => [node.name, node.status, node.cpu, node.memory])}
+ />
+ </Card>
+ </div>
+ <Card title="Pod Status">
+ <StatusTable
+ headers={['Name', 'Status', 'Image']}
+ rows={pods.map(pod => [pod.name, pod.status, pod.image])}
+ />
+ </Card>
+ </div>
+ </main>
+ </div>
+ );
+};
+
+export default Dashboard;
diff --git a/frontend/src/components/ImageProcessor.tsx b/frontend/src/components/ImageProcessor.tsx
index bdcae19..315f6f4 100644
--- a/frontend/src/components/ImageProcessor.tsx
+++ b/frontend/src/components/ImageProcessor.tsx
@@ -1,18 +1,28 @@
import React, { useState } from 'react';
+import { SelectValue, SelectTrigger, SelectItem, SelectContent, Select } from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
const BACK_END_URL = 'http://localhost:5000'
+// const BACK_END_URL = '<link of kubernetes loadbalancer>';
-const ImageProcessor = (): JSX.Element => {
+type Operation = 'edge_detection' | 'color_inversion' | 'grayscale' | 'blur' | 'sharpen' | 'brightness_increase' | 'contrast_increase' | 'sharpening';
+
+export default function Component() {
const [file, setFile] = useState<File | null>(null);
+ const [filePreview, setFilePreview] = useState<string>('https://placehold.jp/1000x1000.png');
const [downloadUrl, setDownloadUrl] = useState<string>('');
+ const [operation, setOperation] = useState<Operation>('edge_detection');
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
if (event.target.files) {
- setFile(event.target.files[0]);
+ const selectedFile = event.target.files[0];
+ setFile(selectedFile);
+ setFilePreview(URL.createObjectURL(selectedFile));
}
};
- const processImage = async (operation: 'edge_detection' | 'color_inversion'): Promise<void> => {
+ const processImage = async (): Promise<void> => {
if (!file) {
alert('Please select a file first!');
return;
@@ -31,7 +41,7 @@ const ImageProcessor = (): JSX.Element => {
const data = await response.json();
if (response.ok) {
setDownloadUrl(`${data.processed_file}`);
- console.log(data.processed_file)
+ console.log(data.processed_file);
alert('File processed successfully!');
} else {
alert(data.error || 'Failed to process the file');
@@ -50,26 +60,37 @@ const ImageProcessor = (): JSX.Element => {
};
return (
- <div className="p-8 bg-white rounded-lg shadow-md flex flex-col items-center">
- <h2 className="text-2xl font-bold mb-4">Image Processing</h2>
- <div className="mb-4">
- <label className="block mb-2">Upload an image</label>
- <input type="file" onChange={handleFileChange} className="mb-4" />
+ <div className="flex flex-col items-center justify-center h-screen w-full dark:bg-gray-950">
+ <Select className="dark:bg-gray-800 dark:text-gray-50 mb-4" defaultValue="edge_detection" onValueChange={(value) => setOperation(value as Operation)}>
+ <SelectTrigger className="w-64 dark:bg-gray-800 dark:text-gray-50">
+ <SelectValue placeholder="Select Operation" />
+ </SelectTrigger>
+ <SelectContent className="dark:bg-gray-800 dark:text-gray-50">
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="edge_detection">Edge Detection</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="color_inversion">Color Inversion</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="grayscale">Grayscale</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="blur">Blur</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="sharpen">Sharpen</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="brightness_increase">Brightness Increase</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="contrast_increase">Contrast Increase</SelectItem>
+ <SelectItem className="dark:hover:bg-gray-700 dark:hover:text-gray-50" value="sharpening">Sharpening</SelectItem>
+ </SelectContent>
+ </Select>
+ <div className="flex gap-4 mb-4">
+ <Button className="dark:bg-gray-800 dark:text-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-50" size="lg" variant="outline" onClick={() => document.getElementById('fileInput')?.click()}>
+ Upload Image
+ <Input id="fileInput" className="hidden" type="file" onChange={handleFileChange} />
+ </Button>
+ <Button className="dark:bg-gray-800 dark:text-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-50" size="lg" variant="outline" onClick={processImage}>
+ Process Image
+ </Button>
+ <Button className="dark:bg-gray-800 dark:text-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-50" size="lg" variant="outline" onClick={downloadImage}>
+ Download Image
+ </Button>
</div>
- <div className="flex flex-row justify-between w-full mb-4">
- <button onClick={() => processImage('edge_detection')} className="bg-black text-white font-bold py-2 px-4 rounded w-full mr-2">
- Edge Detection
- </button>
- <button onClick={() => processImage('color_inversion')} className="bg-black text-white font-bold py-2 px-4 rounded w-full ml-2">
- Color Inversion
- </button>
+ <div className="w-full max-w-2xl">
+ <img alt="Processed Image" className="rounded-md" height={600} src={downloadUrl || filePreview} style={{ aspectRatio: "800/600", objectFit: "cover" }} width={800} />
</div>
- <img className='border-8 border-red-50' src={downloadUrl == '' ? "https://placehold.co/600x400" : downloadUrl} />
- <button onClick={downloadImage} className="bg-black text-white font-bold py-2 px-4 rounded w-full">
- Download
- </button>
</div>
);
-};
-
-export default ImageProcessor;
+}
diff --git a/frontend/src/components/ImageSideBar.tsx b/frontend/src/components/ImageSideBar.tsx
new file mode 100644
index 0000000..c2ec33f
--- /dev/null
+++ b/frontend/src/components/ImageSideBar.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+type ImageSidebarProps = {
+ name: string;
+ previews: string[];
+ files: File[];
+};
+
+const ImageSidebar: React.FC<ImageSidebarProps> = ({ name, previews, files }) => {
+ return (
+ <div className="w-64 flex-shrink-0 overflow-y-auto border-l-2 border-gray-400 bg-blue-100 shadow-xl p-4">
+ <h4 className="font-semibold text-lg text-blue-900 mb-4">{name}</h4>
+ {previews.map((preview, index) => (
+ <div key={index} className="mb-4">
+ <p className="text-sm font-medium text-gray-800">{files[index].name}</p>
+ <img src={preview} alt={files[index].name} style={{ width: '100%', height: 'auto' }} />
+ </div>
+ ))}
+ </div>
+ );
+};
+
+export default ImageSidebar;
+
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
new file mode 100644
index 0000000..0ba4277
--- /dev/null
+++ b/frontend/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "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",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+ VariantProps<typeof buttonVariants> {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+ <Comp
+ className={cn(buttonVariants({ variant, size, className }))}
+ ref={ref}
+ {...props}
+ />
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx
new file mode 100644
index 0000000..677d05f
--- /dev/null
+++ b/frontend/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
+
+const Input = React.forwardRef<HTMLInputElement, InputProps>(
+ ({ className, type, ...props }, ref) => {
+ return (
+ <input
+ type={type}
+ className={cn(
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ className
+ )}
+ ref={ref}
+ {...props}
+ />
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx
new file mode 100644
index 0000000..fe56d4d
--- /dev/null
+++ b/frontend/src/components/ui/select.tsx
@@ -0,0 +1,158 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
+>(({ className, children, ...props }, ref) => (
+ <SelectPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <SelectPrimitive.Icon asChild>
+ <ChevronDown className="h-4 w-4 opacity-50" />
+ </SelectPrimitive.Icon>
+ </SelectPrimitive.Trigger>
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.ScrollUpButton
+ ref={ref}
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className
+ )}
+ {...props}
+ >
+ <ChevronUp className="h-4 w-4" />
+ </SelectPrimitive.ScrollUpButton>
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.ScrollDownButton
+ ref={ref}
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className
+ )}
+ {...props}
+ >
+ <ChevronDown className="h-4 w-4" />
+ </SelectPrimitive.ScrollDownButton>
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
+>(({ className, children, position = "popper", ...props }, ref) => (
+ <SelectPrimitive.Portal>
+ <SelectPrimitive.Content
+ ref={ref}
+ className={cn(
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ position === "popper" &&
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+ className
+ )}
+ position={position}
+ {...props}
+ >
+ <SelectScrollUpButton />
+ <SelectPrimitive.Viewport
+ className={cn(
+ "p-1",
+ position === "popper" &&
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
+ )}
+ >
+ {children}
+ </SelectPrimitive.Viewport>
+ <SelectScrollDownButton />
+ </SelectPrimitive.Content>
+ </SelectPrimitive.Portal>
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.Label
+ ref={ref}
+ className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
+ {...props}
+ />
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
+>(({ className, children, ...props }, ref) => (
+ <SelectPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <SelectPrimitive.ItemIndicator>
+ <Check className="h-4 w-4" />
+ </SelectPrimitive.ItemIndicator>
+ </span>
+
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+ </SelectPrimitive.Item>
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
+ {...props}
+ />
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index b5c61c9..b0e6fff 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,3 +1,76 @@
@tailwind base;
-@tailwind components;
-@tailwind utilities;
+ @tailwind components;
+ @tailwind utilities;
+
+ @layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+ }
+
+ @layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ } \ No newline at end of file
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..d084cca
--- /dev/null
+++ b/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}