mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-01 08:34:53 +02:00
add license system and ui
This commit is contained in:
parent
80d76befc9
commit
4819f410e6
46 changed files with 2159 additions and 94 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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 />
|
||||
|
|
42
src/components/ProfessionalContentOverlay.tsx
Normal file
42
src/components/ProfessionalContentOverlay.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
30
src/components/ui/progress.tsx
Normal file
30
src/components/ui/progress.tsx
Normal 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 };
|
Loading…
Add table
Add a link
Reference in a new issue