re-skin
This commit is contained in:
parent
92408f3617
commit
961723856b
19 changed files with 1117 additions and 377 deletions
42
frontend/components/discord-link.tsx
Normal file
42
frontend/components/discord-link.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Users } from "lucide-react";
|
||||
import { useDiscordMembers } from "@/hooks/use-discord-members";
|
||||
import { formatCount } from "@/lib/format-count";
|
||||
|
||||
interface DiscordLinkProps {
|
||||
inviteCode?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DiscordLink = React.forwardRef<HTMLAnchorElement, DiscordLinkProps>(
|
||||
({ inviteCode = "EqksyE2EX9", className }, ref) => {
|
||||
const { data, isLoading, error } = useDiscordMembers(inviteCode);
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
href={`https://discord.gg/${inviteCode}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.120.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
<span className="hidden sm:inline ml-2">
|
||||
{isLoading ? "..." : error ? "--" : data ? formatCount(data.approximate_member_count) : "--"}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DiscordLink.displayName = "DiscordLink";
|
||||
|
||||
export { DiscordLink };
|
||||
103
frontend/components/file-upload-area.tsx
Normal file
103
frontend/components/file-upload-area.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Upload, FolderOpen, Loader2 } from "lucide-react"
|
||||
|
||||
interface FileUploadAreaProps {
|
||||
onFileSelected?: (file: File) => void
|
||||
isLoading?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FileUploadArea = React.forwardRef<HTMLDivElement, FileUploadAreaProps>(
|
||||
({ onFileSelected, isLoading = false, className }, ref) => {
|
||||
const [isDragging, setIsDragging] = React.useState(false)
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
if (files.length > 0 && onFileSelected) {
|
||||
onFileSelected(files[0])
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length > 0 && onFileSelected) {
|
||||
onFileSelected(files[0])
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isLoading) {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex min-h-[150px] w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-background p-6 text-center transition-colors hover:bg-muted/50",
|
||||
isDragging && "border-primary bg-primary/5",
|
||||
isLoading && "cursor-not-allowed opacity-50",
|
||||
className
|
||||
)}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{isLoading && (
|
||||
<div className="rounded-full bg-muted p-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
{isLoading ? "Processing file..." : "Drop files here or click to upload"}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isLoading ? "Please wait while your file is being processed" : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isLoading && (
|
||||
<Button size="sm">
|
||||
+ Upload
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FileUploadArea.displayName = "FileUploadArea"
|
||||
|
||||
export { FileUploadArea }
|
||||
40
frontend/components/github-star-button.tsx
Normal file
40
frontend/components/github-star-button.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Github, Star, TrendingUp } from "lucide-react";
|
||||
import { useGitHubStars } from "@/hooks/use-github-stars";
|
||||
import { formatCount, formatExactCount } from "@/lib/format-count";
|
||||
|
||||
interface GitHubStarButtonProps {
|
||||
repo?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const GitHubStarButton = React.forwardRef<HTMLAnchorElement, GitHubStarButtonProps>(
|
||||
({ repo = "phact/openrag", className }, ref) => {
|
||||
const { data, isLoading, error } = useGitHubStars(repo);
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
href={`https://github.com/${repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">
|
||||
{isLoading ? "..." : error ? "--" : data ? formatCount(data.stargazers_count) : "--"}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GitHubStarButton.displayName = "GitHubStarButton";
|
||||
|
||||
export { GitHubStarButton };
|
||||
|
|
@ -30,7 +30,7 @@ export function Navigation() {
|
|||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4 py-4 flex flex-col h-full bg-card">
|
||||
<div className="space-y-4 py-4 flex flex-col h-full bg-background">
|
||||
<div className="px-3 py-2 flex-1">
|
||||
<div className="space-y-1">
|
||||
{routes.map((route) => (
|
||||
|
|
|
|||
|
|
@ -1,59 +1,88 @@
|
|||
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"
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-70 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-white text-black shadow-xs hover:bg-gray-100",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border border-border/40 bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary-hover",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input hover:bg-input hover:text-accent-foreground",
|
||||
primary: "border bg-background text-secondary-foreground hover:bg-muted hover:shadow-sm",
|
||||
warning: "bg-warning-foreground text-warning-text hover:bg-warning-foreground/90 hover:shadow-sm",
|
||||
secondary: "border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
|
||||
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground disabled:!bg-transparent",
|
||||
ghostActive: "bg-muted text-foreground hover:bg-secondary-hover hover:text-accent-foreground",
|
||||
link: "underline-offset-4 hover:underline text-primary",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
default: "h-10 py-2 px-4",
|
||||
md: "h-8 py-2 px-4",
|
||||
sm: "h-9 px-3 rounded-md",
|
||||
xs: "py-0.5 px-3 rounded-md",
|
||||
lg: "h-11 px-8 rounded-md",
|
||||
iconMd: "p-1.5 rounded-md",
|
||||
icon: "p-1 rounded-md",
|
||||
iconSm: "p-0.5 rounded-md",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function toTitleCase(text: string) {
|
||||
return text
|
||||
?.split(" ")
|
||||
?.map((word) => word?.charAt(0)?.toUpperCase() + word?.slice(1)?.toLowerCase())
|
||||
?.join(" ");
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
loading?: boolean;
|
||||
ignoreTitleCase?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, loading, disabled, asChild = false, children, ignoreTitleCase = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
let newChildren = children;
|
||||
if (typeof children === "string") {
|
||||
newChildren = ignoreTitleCase ? children : toTitleCase(children);
|
||||
}
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={buttonVariants({ variant, size, className })}
|
||||
disabled={loading || disabled}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<span className="relative flex items-center justify-center">
|
||||
<span className="invisible flex items-center justify-center gap-2">
|
||||
{newChildren}
|
||||
</span>
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
newChildren
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
|||
|
|
@ -1,92 +1,58 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
data-slot="card"
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-border/40 py-6 shadow-sm",
|
||||
className
|
||||
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-4", className)} {...props} />
|
||||
)
|
||||
);
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-base font-semibold leading-tight tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-4 pt-0", className)} {...props} />
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Card.displayName = "Card";
|
||||
CardHeader.displayName = "CardHeader";
|
||||
CardTitle.displayName = "CardTitle";
|
||||
CardDescription.displayName = "CardDescription";
|
||||
CardContent.displayName = "CardContent";
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||
|
|
|
|||
|
|
@ -1,21 +1,46 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-border/40 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
icon?: React.ReactNode;
|
||||
inputClassName?: string;
|
||||
}
|
||||
|
||||
export { Input }
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, inputClassName, icon, type, placeholder, ...props }, ref) => {
|
||||
return (
|
||||
<label className={cn("relative block h-fit w-full text-sm", icon ? className : "")}>
|
||||
{icon && (
|
||||
<div className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-muted-foreground">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
autoComplete="off"
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
"primary-input !placeholder-transparent",
|
||||
icon && "pl-9",
|
||||
icon ? inputClassName : className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none absolute top-1/2 -translate-y-1/2 pl-px text-placeholder-foreground",
|
||||
icon ? "left-9" : "left-3",
|
||||
props.value && "hidden",
|
||||
)}
|
||||
>
|
||||
{placeholder}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
|
|
|
|||
22
frontend/components/ui/scroll-area.tsx
Normal file
22
frontend/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("overflow-auto scrollbar-hide", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ScrollArea.displayName = "ScrollArea";
|
||||
|
||||
export { ScrollArea };
|
||||
54
frontend/hooks/use-discord-members.ts
Normal file
54
frontend/hooks/use-discord-members.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import * as React from "react";
|
||||
|
||||
interface DiscordData {
|
||||
approximate_member_count: number;
|
||||
approximate_presence_count: number;
|
||||
guild: {
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useDiscordMembers = (inviteCode: string) => {
|
||||
const [data, setData] = React.useState<DiscordData | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchDiscordData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/v10/invites/${inviteCode}?with_counts=true&with_expiration=true`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Discord API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const discordData = await response.json();
|
||||
setData(discordData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch Discord data');
|
||||
console.error('Discord API Error:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDiscordData();
|
||||
|
||||
// Refresh every 10 minutes
|
||||
const interval = setInterval(fetchDiscordData, 10 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [inviteCode]);
|
||||
|
||||
return { data, isLoading, error };
|
||||
};
|
||||
50
frontend/hooks/use-github-stars.ts
Normal file
50
frontend/hooks/use-github-stars.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from "react";
|
||||
|
||||
interface GitHubData {
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
open_issues_count: number;
|
||||
}
|
||||
|
||||
export const useGitHubStars = (repo: string) => {
|
||||
const [data, setData] = React.useState<GitHubData | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchGitHubData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`https://api.github.com/repos/${repo}`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
// Optional: Add your GitHub token for higher rate limits
|
||||
// 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_GITHUB_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const repoData = await response.json();
|
||||
setData(repoData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch GitHub data');
|
||||
console.error('GitHub API Error:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchGitHubData();
|
||||
|
||||
// Refresh every 5 minutes
|
||||
const interval = setInterval(fetchGitHubData, 5 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [repo]);
|
||||
|
||||
return { data, isLoading, error };
|
||||
};
|
||||
13
frontend/lib/format-count.ts
Normal file
13
frontend/lib/format-count.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export const formatCount = (count: number): string => {
|
||||
if (count >= 1_000_000) {
|
||||
return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
if (count >= 1_000) {
|
||||
return `${(count / 1_000).toFixed(1)}k`;
|
||||
}
|
||||
return count.toLocaleString();
|
||||
};
|
||||
|
||||
export const formatExactCount = (count: number): string => {
|
||||
return count.toLocaleString();
|
||||
};
|
||||
64
frontend/package-lock.json
generated
64
frontend/package-lock.json
generated
|
|
@ -17,6 +17,8 @@
|
|||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
|
@ -1868,6 +1870,46 @@
|
|||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
||||
"integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
||||
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.castarray": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
|
|
@ -5211,11 +5253,22 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
|
|
@ -5278,6 +5331,15 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
|
|
|||
|
|
@ -4,99 +4,317 @@
|
|||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--font-chivo: "Chivo", sans-serif;
|
||||
|
||||
/* Core Theme Colors */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--foreground: 0 0% 0%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card-foreground: 0 0% 0%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--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: 217.2 91.2% 59.8%;
|
||||
--radius: 0.65rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--popover-foreground: 0 0% 0%;
|
||||
--primary: 0 0% 0%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-hover: 240 4% 16%;
|
||||
--secondary: 0 0% 100%;
|
||||
--secondary-foreground: 240 4% 16%;
|
||||
--secondary-hover: 240 6% 90%;
|
||||
--muted: 240 5% 96%;
|
||||
--muted-foreground: 240 4% 46%;
|
||||
--accent: 240 5% 96%;
|
||||
--accent-foreground: 0 0% 0%;
|
||||
--destructive: 0 72% 51%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 240 6% 90%;
|
||||
--input: 240 6% 90%;
|
||||
--ring: 0 0% 0%;
|
||||
--placeholder-foreground: 240 5% 65%;
|
||||
|
||||
/* Status Colors */
|
||||
--status-red: #ef4444;
|
||||
--status-yellow: #eab308;
|
||||
--status-green: #4ade80;
|
||||
--status-blue: #2563eb;
|
||||
|
||||
/* Component Colors */
|
||||
--component-icon: #d8598a;
|
||||
--flow-icon: #2f67d0;
|
||||
|
||||
/* Data Type Colors */
|
||||
--datatype-blue: 221.2 83.2% 53.3%;
|
||||
--datatype-blue-foreground: 214.3 94.6% 92.7%;
|
||||
--datatype-yellow: 40.6 96.1% 40.4%;
|
||||
--datatype-yellow-foreground: 54.9 96.7% 88%;
|
||||
--datatype-red: 0 72.2% 50.6%;
|
||||
--datatype-red-foreground: 0 93.3% 94.1%;
|
||||
--datatype-emerald: 161.4 93.5% 30.4%;
|
||||
--datatype-emerald-foreground: 149.3 80.4% 90%;
|
||||
--datatype-violet: 262.1 83.3% 57.8%;
|
||||
--datatype-violet-foreground: 251.4 91.3% 95.5%;
|
||||
|
||||
/* Warning */
|
||||
--warning: 48 96.6% 76.7%;
|
||||
--warning-foreground: 240 6% 10%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Main backgrounds - very dark charcoal */
|
||||
--background: 0 0% 10%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
/* Card backgrounds - slightly lighter dark gray */
|
||||
--card: 0 0% 16%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
/* Popover backgrounds */
|
||||
--popover: 0 0% 16%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
/* Primary accent - bright teal/cyan */
|
||||
--primary: 162 100% 42%;
|
||||
--primary-foreground: 0 0% 10%;
|
||||
|
||||
/* Secondary elements - medium gray */
|
||||
--secondary: 215 25% 27%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
/* Muted elements - darker gray */
|
||||
--muted: 215 25% 27%;
|
||||
--muted-foreground: 215 20% 65%;
|
||||
|
||||
/* Accent elements - bright blue */
|
||||
--accent: 0 0% 100%;
|
||||
--accent-foreground: 0 0% 10%;
|
||||
|
||||
/* Destructive/error - red */
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
/* Borders - subtle gray */
|
||||
--border: 215 25% 35%;
|
||||
|
||||
/* Input backgrounds - darker gray */
|
||||
--input: 215 25% 27%;
|
||||
|
||||
/* Ring/focus colors - teal accent */
|
||||
--ring: 162 100% 42%;
|
||||
|
||||
/* Chart colors - adjusted for dark theme */
|
||||
--chart-1: 162 100% 42%;
|
||||
--chart-2: 142 76% 36%;
|
||||
--chart-3: 217 91% 60%;
|
||||
--chart-4: 45 93% 58%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
--background: 240 6% 10%;
|
||||
--foreground: 0 0% 100%;
|
||||
--card: 240 6% 10%;
|
||||
--card-foreground: 0 0% 100%;
|
||||
--popover: 240 6% 10%;
|
||||
--popover-foreground: 0 0% 100%;
|
||||
--primary: 0 0% 100%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--primary-hover: 240 6% 90%;
|
||||
--secondary: 0 0% 0%;
|
||||
--secondary-foreground: 240 6% 90%;
|
||||
--secondary-hover: 240 4% 16%;
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 240 5% 65%;
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 240 5% 26%;
|
||||
--input: 240 5% 34%;
|
||||
--ring: 0 0% 100%;
|
||||
--placeholder-foreground: 240 4% 46%;
|
||||
|
||||
/* Dark mode data type colors */
|
||||
--datatype-blue: 211.7 96.4% 78.4%;
|
||||
--datatype-blue-foreground: 221.2 83.2% 53.3%;
|
||||
--datatype-yellow: 50.4 97.8% 63.5%;
|
||||
--datatype-yellow-foreground: 40.6 96.1% 40.4%;
|
||||
--datatype-red: 0 93.5% 81.8%;
|
||||
--datatype-red-foreground: 0 72.2% 50.6%;
|
||||
--datatype-emerald: 156.2 71.6% 66.9%;
|
||||
--datatype-emerald-foreground: 161.4 93.5% 30.4%;
|
||||
--datatype-violet: 252.5 94.7% 85.1%;
|
||||
--datatype-violet-foreground: 262.1 83.3% 57.8%;
|
||||
|
||||
--warning: 45.9 96.7% 64.5%;
|
||||
--warning-foreground: 240 6% 10%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
@layer components {
|
||||
.header-arrangement {
|
||||
@apply flex w-full h-[53px] items-center justify-between border-b border-border;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
.header-start-display {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
||||
.header-end-division {
|
||||
@apply flex justify-end px-2;
|
||||
}
|
||||
|
||||
.header-end-display {
|
||||
@apply ml-auto mr-2 flex items-center gap-5;
|
||||
}
|
||||
|
||||
.header-github-link {
|
||||
@apply inline-flex h-8 items-center justify-center rounded-md border border-input px-2 text-sm font-medium text-muted-foreground shadow-sm ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
|
||||
}
|
||||
|
||||
.header-notifications {
|
||||
@apply absolute right-[3px] h-1.5 w-1.5 rounded-full bg-destructive;
|
||||
}
|
||||
|
||||
.header-menu-bar {
|
||||
@apply flex items-center rounded-md py-1 text-sm font-medium;
|
||||
}
|
||||
|
||||
.header-menu-bar-display {
|
||||
@apply flex max-w-[110px] cursor-pointer items-center gap-2 lg:max-w-[150px];
|
||||
}
|
||||
|
||||
.header-menu-flow-name {
|
||||
@apply flex-1 truncate;
|
||||
}
|
||||
|
||||
.header-menu-options {
|
||||
@apply mr-2 h-4 w-4;
|
||||
}
|
||||
|
||||
.side-bar-arrangement {
|
||||
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
|
||||
}
|
||||
|
||||
.side-bar-search-div-placement {
|
||||
@apply relative mx-auto flex items-center py-3;
|
||||
}
|
||||
|
||||
.side-bar-components-icon {
|
||||
@apply h-6 w-4 text-ring;
|
||||
}
|
||||
|
||||
.side-bar-components-text {
|
||||
@apply w-full truncate pr-1 text-xs text-foreground;
|
||||
}
|
||||
|
||||
.side-bar-components-div-form {
|
||||
@apply flex w-full items-center justify-between rounded-md rounded-l-none border border-l-0 border-dashed border-ring px-3 py-1 text-sm;
|
||||
}
|
||||
|
||||
.side-bar-components-border {
|
||||
@apply cursor-grab rounded-l-md border-l-8;
|
||||
}
|
||||
|
||||
.side-bar-components-gap {
|
||||
@apply flex flex-col gap-2 p-2;
|
||||
}
|
||||
|
||||
.side-bar-components-div-arrangement {
|
||||
@apply w-full overflow-auto pb-10 scrollbar-hide;
|
||||
}
|
||||
|
||||
.side-bar-button-size {
|
||||
@apply h-5 w-5;
|
||||
}
|
||||
|
||||
.side-bar-button-size:hover {
|
||||
@apply hover:text-accent-foreground;
|
||||
}
|
||||
|
||||
.side-bar-buttons-arrangement {
|
||||
@apply mb-2 mt-2 flex w-full items-center justify-between gap-2 px-2;
|
||||
}
|
||||
|
||||
.side-bar-button {
|
||||
@apply flex w-full;
|
||||
}
|
||||
|
||||
.components-disclosure-arrangement {
|
||||
@apply -mt-px flex w-full select-none items-center justify-between border-y border-y-input bg-primary-foreground px-3 py-2;
|
||||
}
|
||||
|
||||
.components-disclosure-title {
|
||||
@apply flex items-center text-sm text-primary;
|
||||
}
|
||||
|
||||
.toolbar-wrapper {
|
||||
@apply flex h-10 items-center gap-1 rounded-xl border border-border bg-background p-1 shadow-sm;
|
||||
}
|
||||
|
||||
.playground-btn-flow-toolbar {
|
||||
@apply relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-normal transition-all duration-500 ease-in-out;
|
||||
}
|
||||
|
||||
.input-search {
|
||||
@apply primary-input mx-2 pr-7;
|
||||
}
|
||||
|
||||
/* GitHub Star Button */
|
||||
.github-star-link {
|
||||
@apply inline-flex h-8 items-center rounded-md text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
|
||||
}
|
||||
|
||||
.github-star-content {
|
||||
@apply flex items-center px-2 pr-1;
|
||||
}
|
||||
|
||||
.github-star-icon {
|
||||
@apply h-4 w-4;
|
||||
}
|
||||
|
||||
.github-star-text {
|
||||
@apply ml-1;
|
||||
}
|
||||
|
||||
.github-star-count-section {
|
||||
@apply border-l bg-muted/80 px-2 py-1;
|
||||
}
|
||||
|
||||
.github-star-display {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.github-star-count-icon {
|
||||
@apply h-3 w-3 fill-current;
|
||||
}
|
||||
|
||||
.github-star-count {
|
||||
@apply text-xs font-medium text-foreground;
|
||||
}
|
||||
|
||||
.github-star-loading {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.github-star-skeleton {
|
||||
@apply h-3 w-8 animate-pulse bg-muted-foreground/20 rounded;
|
||||
}
|
||||
|
||||
.github-star-error {
|
||||
@apply text-xs text-muted-foreground;
|
||||
}
|
||||
|
||||
/* Discord Link */
|
||||
.discord-link {
|
||||
@apply inline-flex h-8 items-center rounded-md text-sm font-medium shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
|
||||
background-color: #5865f2;
|
||||
border-color: #5865f2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.discord-link:hover {
|
||||
@apply opacity-90;
|
||||
}
|
||||
|
||||
.discord-content {
|
||||
@apply flex items-center px-2 pr-1;
|
||||
}
|
||||
|
||||
.discord-icon {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.discord-text {
|
||||
@apply ml-1;
|
||||
}
|
||||
|
||||
.discord-member-section {
|
||||
@apply border-l border-white/20 bg-black/10 px-2 py-1;
|
||||
}
|
||||
|
||||
.discord-member-display {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.discord-member-icon {
|
||||
@apply h-3 w-3;
|
||||
}
|
||||
|
||||
.discord-member-count {
|
||||
@apply text-xs font-medium;
|
||||
}
|
||||
|
||||
.discord-loading {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.discord-skeleton {
|
||||
@apply h-3 w-8 animate-pulse bg-white/20 rounded;
|
||||
}
|
||||
|
||||
.discord-error {
|
||||
@apply text-xs opacity-70;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Upload, FolderOpen, Loader2, PlugZap, RefreshCw, Download } from "lucid
|
|||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
import { useTask } from "@/contexts/task-context"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { FileUploadArea } from "@/components/file-upload-area"
|
||||
|
||||
type FacetBucket = { key: string; count: number }
|
||||
|
||||
|
|
@ -48,7 +49,6 @@ function KnowledgeSourcesPage() {
|
|||
// File upload state
|
||||
const [fileUploadLoading, setFileUploadLoading] = useState(false)
|
||||
const [pathUploadLoading, setPathUploadLoading] = useState(false)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [folderPath, setFolderPath] = useState("/app/documents/")
|
||||
const [uploadStatus, setUploadStatus] = useState<string>("")
|
||||
|
||||
|
|
@ -66,16 +66,13 @@ function KnowledgeSourcesPage() {
|
|||
const [facetStats, setFacetStats] = useState<{ data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[] } | null>(null)
|
||||
|
||||
// File upload handlers
|
||||
const handleFileUpload = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!selectedFile) return
|
||||
|
||||
const handleDirectFileUpload = async (file: File) => {
|
||||
setFileUploadLoading(true)
|
||||
setUploadStatus("")
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("file", selectedFile)
|
||||
formData.append("file", file)
|
||||
|
||||
const response = await fetch("/api/upload", {
|
||||
method: "POST",
|
||||
|
|
@ -86,9 +83,6 @@ function KnowledgeSourcesPage() {
|
|||
|
||||
if (response.ok) {
|
||||
setUploadStatus(`File processed successfully! ID: ${result.id}`)
|
||||
setSelectedFile(null)
|
||||
const fileInput = document.getElementById("file-input") as HTMLInputElement
|
||||
if (fileInput) fileInput.value = ""
|
||||
|
||||
// Refresh stats after successful file upload
|
||||
fetchStats()
|
||||
|
|
@ -331,7 +325,7 @@ function KnowledgeSourcesPage() {
|
|||
case "error":
|
||||
return <Badge variant="destructive">Error</Badge>
|
||||
default:
|
||||
return <Badge variant="outline" className="bg-muted/20 text-muted-foreground border-muted">Not Connected</Badge>
|
||||
return <Badge variant="outline" className="bg-muted/20 text-muted-foreground border-muted whitespace-nowrap">Not Connected</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -507,7 +501,7 @@ function KnowledgeSourcesPage() {
|
|||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* File Upload Card */}
|
||||
<Card>
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-5 w-5" />
|
||||
|
|
@ -517,40 +511,16 @@ function KnowledgeSourcesPage() {
|
|||
Import a single document to be processed and indexed
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleFileUpload} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="file-input">File</Label>
|
||||
<Input
|
||||
id="file-input"
|
||||
type="file"
|
||||
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
||||
accept=".pdf,.docx,.txt,.md"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!selectedFile || fileUploadLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{fileUploadLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Add File
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
<CardContent className="flex-1 flex flex-col justify-end">
|
||||
<FileUploadArea
|
||||
onFileSelected={handleDirectFileUpload}
|
||||
isLoading={fileUploadLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Folder Upload Card */}
|
||||
<Card>
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FolderOpen className="h-5 w-5" />
|
||||
|
|
@ -560,7 +530,7 @@ function KnowledgeSourcesPage() {
|
|||
Process all documents in a folder path
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex-1 flex flex-col justify-end">
|
||||
<form onSubmit={handlePathUpload} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="folder-path">Folder Path</Label>
|
||||
|
|
@ -626,22 +596,24 @@ function KnowledgeSourcesPage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Label htmlFor="maxFiles" className="text-sm font-medium">
|
||||
Max files per sync:
|
||||
</Label>
|
||||
<Input
|
||||
id="maxFiles"
|
||||
type="number"
|
||||
value={maxFiles}
|
||||
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
|
||||
className="w-24"
|
||||
min="1"
|
||||
max="100"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
(Leave blank or set to 0 for unlimited)
|
||||
</span>
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="maxFiles" className="font-medium whitespace-nowrap">
|
||||
Max files per sync:
|
||||
</Label>
|
||||
<Input
|
||||
id="maxFiles"
|
||||
type="number"
|
||||
value={maxFiles}
|
||||
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
|
||||
className="w-16 min-w-16 max-w-16 flex-shrink-0"
|
||||
min="1"
|
||||
max="100"
|
||||
/>
|
||||
<span className="text-muted-foreground whitespace-nowrap">
|
||||
(Leave blank or set to 0 for unlimited)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -650,7 +622,7 @@ function KnowledgeSourcesPage() {
|
|||
{/* Connectors Grid */}
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{connectors.map((connector) => (
|
||||
<Card key={connector.id} className="relative">
|
||||
<Card key={connector.id} className="relative flex flex-col">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -665,7 +637,7 @@ function KnowledgeSourcesPage() {
|
|||
{getStatusBadge(connector.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="flex-1 flex flex-col justify-end space-y-4">
|
||||
{connector.status === "connected" ? (
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Inter, JetBrains_Mono, Chivo } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { AuthProvider } from "@/contexts/auth-context";
|
||||
|
|
@ -8,13 +8,18 @@ import { KnowledgeFilterProvider } from "@/contexts/knowledge-filter-context";
|
|||
import { LayoutWrapper } from "@/components/layout-wrapper";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const inter = Inter({
|
||||
variable: "--font-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const chivo = Chivo({
|
||||
variable: "--font-chivo",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
|
|
@ -30,8 +35,14 @@ export default function RootLayout({
|
|||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Chivo:ital,wght@0,100..900;1,100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${inter.variable} ${jetbrainsMono.variable} ${chivo.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import { Bell, BellRing } from "lucide-react"
|
|||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Navigation } from "@/components/navigation"
|
||||
import { ModeToggle } from "@/components/mode-toggle"
|
||||
import { UserNav } from "@/components/user-nav"
|
||||
import { TaskNotificationMenu } from "@/components/task-notification-menu"
|
||||
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown"
|
||||
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel"
|
||||
import { GitHubStarButton } from "@/components/github-star-button"
|
||||
import { DiscordLink } from "@/components/discord-link"
|
||||
import { useTask } from "@/contexts/task-context"
|
||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"
|
||||
|
||||
|
|
@ -36,60 +37,64 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
|||
)
|
||||
}
|
||||
|
||||
// For all other pages, render with full navigation and task menu
|
||||
// For all other pages, render with Langflow-styled navigation and task menu
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background">
|
||||
<div className="flex h-14 items-center px-4">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" viewBox="0 0 24 22" fill="currentColor" className="h-6 w-6 mr-2 text-white">
|
||||
<header className="header-arrangement bg-background sticky top-0 z-50">
|
||||
<div className="header-start-display px-4">
|
||||
{/* Logo/Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" viewBox="0 0 24 22" fill="currentColor" className="h-6 w-6 text-primary">
|
||||
<path d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"></path>
|
||||
<path d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"></path>
|
||||
<path d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"></path>
|
||||
</svg>
|
||||
<h1 className="text-lg font-semibold tracking-tight text-white">
|
||||
OpenRAG
|
||||
</h1>
|
||||
<span className="text-lg font-semibold">OpenRAG</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end space-x-2">
|
||||
<nav className="flex items-center space-x-2">
|
||||
{/* Knowledge Filter Dropdown */}
|
||||
<KnowledgeFilterDropdown
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterSelect={setSelectedFilter}
|
||||
/>
|
||||
{/* Task Notification Bell */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleMenu}
|
||||
className="relative p-2"
|
||||
>
|
||||
{activeTasks.length > 0 ? (
|
||||
<BellRing className="h-4 w-4 text-blue-500" />
|
||||
) : (
|
||||
<Bell className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
{activeTasks.length > 0 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs bg-blue-500 text-white border-0"
|
||||
>
|
||||
{activeTasks.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
<UserNav />
|
||||
<ModeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
<div className="header-end-division">
|
||||
<div className="header-end-display">
|
||||
{/* Knowledge Filter Dropdown */}
|
||||
<KnowledgeFilterDropdown
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterSelect={setSelectedFilter}
|
||||
/>
|
||||
|
||||
{/* GitHub Star Button */}
|
||||
<GitHubStarButton repo="phact/openrag" />
|
||||
|
||||
{/* Discord Link */}
|
||||
<DiscordLink inviteCode="EqksyE2EX9" />
|
||||
|
||||
{/* Task Notification Bell */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="iconSm"
|
||||
onClick={toggleMenu}
|
||||
className="relative"
|
||||
>
|
||||
{activeTasks.length > 0 ? (
|
||||
<BellRing className="h-4 w-4 text-blue-500" />
|
||||
) : (
|
||||
<Bell className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
{activeTasks.length > 0 && (
|
||||
<div className="header-notifications" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-6 bg-border" />
|
||||
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="hidden md:flex md:w-72 md:flex-col md:fixed md:top-14 md:bottom-0 md:left-0 z-[80] border-r border-border/40">
|
||||
<div className="side-bar-arrangement bg-background fixed left-0 top-[53px] bottom-0 md:flex hidden">
|
||||
<Navigation />
|
||||
</div>
|
||||
<main className={`md:pl-72 ${(isMenuOpen || isPanelOpen) ? 'md:pr-80' : ''}`}>
|
||||
<div className="flex flex-col h-[calc(100vh-3.6rem)]">
|
||||
<main className={`md:pl-72 md:pr-6 ${(isMenuOpen || isPanelOpen) ? 'md:pr-80' : ''}`}>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<div className="flex-1 overflow-y-auto scrollbar-hide">
|
||||
<div className="container py-6 lg:py-8">
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { LogIn, LogOut, User } from "lucide-react"
|
||||
import { LogIn, LogOut, User, Moon, Sun, Settings, ChevronsUpDown } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
export function UserNav() {
|
||||
const { user, isLoading, isAuthenticated, login, logout } = useAuth()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -39,13 +41,14 @@ export function UserNav() {
|
|||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<Button variant="ghost" className="flex items-center gap-1 h-8 px-1 rounded-full">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={user?.picture} alt={user?.name} />
|
||||
<AvatarFallback>
|
||||
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-4 w-4" />}
|
||||
<AvatarFallback className="text-xs">
|
||||
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-3 w-3" />}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<ChevronsUpDown className="h-3 w-3 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
|
|
@ -58,6 +61,23 @@ export function UserNav() {
|
|||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
|
||||
{theme === "light" ? (
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span>Toggle Theme</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout} className="text-red-600 focus:text-red-600">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import tailwindcssForms from "@tailwindcss/forms";
|
||||
import tailwindcssTypography from "@tailwindcss/typography";
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
import tailwindcssAnimate from "tailwindcss-animate";
|
||||
|
||||
const config: Config = {
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
|
|
@ -8,16 +13,47 @@ const config: Config = {
|
|||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
"3xl": "1500px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
screens: {
|
||||
xl: "1200px",
|
||||
"2xl": "1400px",
|
||||
"3xl": "1500px",
|
||||
},
|
||||
keyframes: {
|
||||
overlayShow: {
|
||||
from: { opacity: 0 },
|
||||
to: { opacity: 1 },
|
||||
},
|
||||
contentShow: {
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: "translate(-50%, -50%) scale(0.95)",
|
||||
clipPath: "inset(50% 0)",
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
clipPath: "inset(0% 0)",
|
||||
},
|
||||
},
|
||||
wiggle: {
|
||||
"0%, 100%": { transform: "scale(100%)" },
|
||||
"50%": { transform: "scale(120%)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
overlayShow: "overlayShow 400ms cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
contentShow: "contentShow 400ms cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
wiggle: "wiggle 150ms ease-in-out 1",
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
|
|
@ -27,10 +63,12 @@ const config: Config = {
|
|||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
hover: "hsl(var(--primary-hover))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
hover: "hsl(var(--secondary-hover))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
|
|
@ -52,37 +90,36 @@ const config: Config = {
|
|||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
// Custom colors from the screenshot
|
||||
teal: {
|
||||
50: "#f0fdfa",
|
||||
100: "#ccfbf1",
|
||||
200: "#99f6e4",
|
||||
300: "#5eead4",
|
||||
400: "#2dd4bf",
|
||||
500: "#14b8a6",
|
||||
600: "#0d9488",
|
||||
700: "#0f766e",
|
||||
800: "#115e59",
|
||||
900: "#134e4a",
|
||||
DEFAULT: "#00D4AA", // Primary teal from screenshot
|
||||
"status-blue": "var(--status-blue)",
|
||||
"status-green": "var(--status-green)",
|
||||
"status-red": "var(--status-red)",
|
||||
"status-yellow": "var(--status-yellow)",
|
||||
"component-icon": "var(--component-icon)",
|
||||
"flow-icon": "var(--flow-icon)",
|
||||
"placeholder-foreground": "hsl(var(--placeholder-foreground))",
|
||||
"datatype-blue": {
|
||||
DEFAULT: "hsl(var(--datatype-blue))",
|
||||
foreground: "hsl(var(--datatype-blue-foreground))",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "#10B981", // Success green from screenshot
|
||||
foreground: "#ffffff",
|
||||
"datatype-yellow": {
|
||||
DEFAULT: "hsl(var(--datatype-yellow))",
|
||||
foreground: "hsl(var(--datatype-yellow-foreground))",
|
||||
},
|
||||
// Additional grays matching the screenshot
|
||||
gray: {
|
||||
850: "#1a1a1a", // Very dark background
|
||||
800: "#2a2a2a", // Card background
|
||||
750: "#333333", // Slightly lighter card
|
||||
700: "#374151", // Input background
|
||||
600: "#4b5563", // Border color
|
||||
500: "#6b7280", // Muted text
|
||||
400: "#9ca3af", // Secondary text
|
||||
300: "#d1d5db",
|
||||
200: "#e5e7eb",
|
||||
100: "#f3f4f6",
|
||||
50: "#f8f9fa", // Primary text
|
||||
"datatype-red": {
|
||||
DEFAULT: "hsl(var(--datatype-red))",
|
||||
foreground: "hsl(var(--datatype-red-foreground))",
|
||||
},
|
||||
"datatype-emerald": {
|
||||
DEFAULT: "hsl(var(--datatype-emerald))",
|
||||
foreground: "hsl(var(--datatype-emerald-foreground))",
|
||||
},
|
||||
"datatype-violet": {
|
||||
DEFAULT: "hsl(var(--datatype-violet))",
|
||||
foreground: "hsl(var(--datatype-violet-foreground))",
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "hsl(var(--warning))",
|
||||
foreground: "hsl(var(--warning-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
|
|
@ -90,23 +127,92 @@ const config: Config = {
|
|||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
mono: ["var(--font-mono)", ...fontFamily.mono],
|
||||
chivo: ["var(--font-chivo)", ...fontFamily.sans],
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
fontSize: {
|
||||
xxs: "11px",
|
||||
mmd: "13px",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [
|
||||
tailwindcssAnimate,
|
||||
tailwindcssForms({
|
||||
strategy: "class",
|
||||
}),
|
||||
plugin(({ addUtilities }) => {
|
||||
addUtilities({
|
||||
".scrollbar-hide": {
|
||||
"-ms-overflow-style": "none",
|
||||
"scrollbar-width": "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
".truncate-multiline": {
|
||||
display: "-webkit-box",
|
||||
"-webkit-line-clamp": "3",
|
||||
"-webkit-box-orient": "vertical",
|
||||
overflow: "hidden",
|
||||
"text-overflow": "ellipsis",
|
||||
},
|
||||
".custom-scroll": {
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
},
|
||||
"&::-webkit-scrollbar-track": {
|
||||
backgroundColor: "hsl(var(--muted))",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "hsl(var(--border))",
|
||||
borderRadius: "999px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: "hsl(var(--placeholder-foreground))",
|
||||
},
|
||||
},
|
||||
".primary-input": {
|
||||
display: "block",
|
||||
width: "100%",
|
||||
borderRadius: "0.375rem",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
paddingLeft: "0.75rem",
|
||||
paddingRight: "0.75rem",
|
||||
paddingTop: "0.5rem",
|
||||
paddingBottom: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
textAlign: "left",
|
||||
textOverflow: "ellipsis",
|
||||
"&::placeholder": {
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
"&:hover": {
|
||||
borderColor: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
"&:focus": {
|
||||
borderColor: "hsl(var(--foreground))",
|
||||
outline: "none",
|
||||
boxShadow: "none",
|
||||
"&::placeholder": {
|
||||
color: "transparent",
|
||||
},
|
||||
},
|
||||
"&:disabled": {
|
||||
pointerEvents: "none",
|
||||
cursor: "not-allowed",
|
||||
backgroundColor: "hsl(var(--muted))",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
tailwindcssTypography,
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
Loading…
Add table
Reference in a new issue