add license system and ui

This commit is contained in:
miloschwartz 2025-04-27 13:03:00 -04:00
parent 80d76befc9
commit 4819f410e6
No known key found for this signature in database
46 changed files with 2159 additions and 94 deletions

View file

@ -4,20 +4,26 @@ import { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Copy, Check } from "lucide-react";
type CopyTextBoxProps = {
text?: string;
displayText?: string;
wrapText?: boolean;
outline?: boolean;
};
export default function CopyTextBox({
text = "",
displayText,
wrapText = false,
outline = true
}) {
}: CopyTextBoxProps) {
const [isCopied, setIsCopied] = useState(false);
const textRef = useRef<HTMLPreElement>(null);
const copyToClipboard = async () => {
if (textRef.current) {
try {
await navigator.clipboard.writeText(
textRef.current.textContent || ""
);
await navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
@ -38,7 +44,7 @@ export default function CopyTextBox({
: "overflow-x-auto"
}`}
>
<code className="block w-full">{text}</code>
<code className="block w-full">{displayText || text}</code>
</pre>
<Button
variant="ghost"

View file

@ -4,10 +4,11 @@ import { useState } from "react";
type CopyToClipboardProps = {
text: string;
displayText?: string;
isLink?: boolean;
};
const CopyToClipboard = ({ text, isLink }: CopyToClipboardProps) => {
const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
@ -19,6 +20,8 @@ const CopyToClipboard = ({ text, isLink }: CopyToClipboardProps) => {
}, 2000);
};
const displayValue = displayText ?? text;
return (
<div className="flex items-center space-x-2 max-w-full">
{isLink ? (
@ -30,7 +33,7 @@ const CopyToClipboard = ({ text, isLink }: CopyToClipboardProps) => {
style={{ maxWidth: "100%" }} // Ensures truncation works within parent
title={text} // Shows full text on hover
>
{text}
{displayValue}
</Link>
) : (
<span
@ -44,7 +47,7 @@ const CopyToClipboard = ({ text, isLink }: CopyToClipboardProps) => {
}}
title={text} // Full text tooltip
>
{text}
{displayValue}
</span>
)}
<button

View file

@ -5,14 +5,19 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { cn } from "@app/lib/cn";
import { buttonVariants } from "@/components/ui/button";
import { Badge } from "@app/components/ui/badge";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
export type HorizontalTabs = Array<{
title: string;
href: string;
icon?: React.ReactNode;
showProfessional?: boolean;
}>;
interface HorizontalTabsProps {
children: React.ReactNode;
items: Array<{
title: string;
href: string;
icon?: React.ReactNode;
}>;
items: HorizontalTabs;
disabled?: boolean;
}
@ -23,6 +28,7 @@ export function HorizontalTabs({
}: HorizontalTabsProps) {
const pathname = usePathname();
const params = useParams();
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
function hydrateHref(href: string) {
return href
@ -42,34 +48,47 @@ export function HorizontalTabs({
const isActive =
pathname.startsWith(hydratedHref) &&
!pathname.includes("create");
const isProfessional =
item.showProfessional && !isUnlocked();
const isDisabled =
disabled || (isProfessional && !isUnlocked());
return (
<Link
key={hydratedHref}
href={hydratedHref}
href={isProfessional ? "#" : hydratedHref}
className={cn(
"px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap",
isActive
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground",
disabled && "cursor-not-allowed"
isDisabled && "cursor-not-allowed"
)}
onClick={
disabled
? (e) => e.preventDefault()
: undefined
}
tabIndex={disabled ? -1 : undefined}
aria-disabled={disabled}
onClick={(e) => {
if (isDisabled) {
e.preventDefault();
}
}}
tabIndex={isDisabled ? -1 : undefined}
aria-disabled={isDisabled}
>
{item.icon ? (
<div className="flex items-center space-x-2">
{item.icon}
<span>{item.title}</span>
</div>
) : (
item.title
)}
<div
className={cn(
"flex items-center space-x-2",
isDisabled && "opacity-60"
)}
>
{item.icon && item.icon}
<span>{item.title}</span>
{isProfessional && (
<Badge
variant="outlinePrimary"
className="ml-2"
>
Professional
</Badge>
)}
</div>
</Link>
);
})}

View file

@ -161,14 +161,6 @@ export function Layout({
>
Documentation
</Link>
<Link
href="mailto:support@fossorial.io"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Support
</Link>
</div>
<div>
<ProfileIcon />

View file

@ -0,0 +1,42 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
"use client";
import { cn } from "@app/lib/cn";
type ProfessionalContentOverlayProps = {
children: React.ReactNode;
isProfessional?: boolean;
};
export function ProfessionalContentOverlay({
children,
isProfessional = false
}: ProfessionalContentOverlayProps) {
return (
<div
className={cn(
"relative",
isProfessional && "opacity-60 pointer-events-none"
)}
>
{isProfessional && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-50">
<div className="text-center p-6 bg-primary/10 rounded-lg">
<h3 className="text-lg font-semibold mb-2">
Professional Edition Required
</h3>
<p className="text-muted-foreground">
This feature is only available in the Professional
Edition.
</p>
</div>
</div>
)}
{children}
</div>
);
}

View file

@ -3,7 +3,7 @@ export function SettingsContainer({ children }: { children: React.ReactNode }) {
}
export function SettingsSection({ children }: { children: React.ReactNode }) {
return <div className="border rounded-lg bg-card p-5">{children}</div>;
return <div className="border rounded-lg bg-card p-5 flex flex-col min-h-[200px]">{children}</div>;
}
export function SettingsSectionHeader({
@ -47,7 +47,7 @@ export function SettingsSectionBody({
}: {
children: React.ReactNode;
}) {
return <div className="space-y-5">{children}</div>;
return <div className="space-y-5 flex-grow">{children}</div>;
}
export function SettingsSectionFooter({
@ -55,7 +55,7 @@ export function SettingsSectionFooter({
}: {
children: React.ReactNode;
}) {
return <div className="flex justify-end space-x-4 mt-8">{children}</div>;
return <div className="flex justify-end space-x-2 mt-auto pt-8">{children}</div>;
}
export function SettingsSectionGrid({

View file

@ -7,6 +7,7 @@ import { cn } from "@app/lib/cn";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useUserContext } from "@app/hooks/useUserContext";
import { Badge } from "@app/components/ui/badge";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
export interface SidebarNavItem {
href: string;
@ -37,6 +38,7 @@ export function SidebarNav({
const resourceId = params.resourceId as string;
const userId = params.userId as string;
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const { user } = useUserContext();
@ -98,7 +100,7 @@ export function SidebarNav({
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedItems.has(hydratedHref);
const indent = level * 28; // Base indent for each level
const isProfessional = item.showProfessional;
const isProfessional = item.showProfessional && !isUnlocked();
const isDisabled = disabled || isProfessional;
return (

View file

@ -16,8 +16,8 @@ const badgeVariants = cva(
destructive:
"border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
green: "border-transparent bg-green-300",
yellow: "border-transparent bg-yellow-300",
green: "border-transparent bg-green-500",
yellow: "border-transparent bg-yellow-500",
red: "border-transparent bg-red-300",
},
},

View file

@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@app/lib/cn";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"border relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };