ADD CROSS DOMAIN SUPPORT

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-06-26 22:35:29 +02:00
parent a0c8199474
commit 41143ca5f8
26 changed files with 514 additions and 126 deletions

View File

@@ -0,0 +1,28 @@
import { clipboard } from '@/utils/clipboard';
import { cn } from '@/utils/cn';
import { CopyIcon } from 'lucide-react';
import { Label } from '../ui/label';
type Props = {
label: React.ReactNode;
value: string;
className?: string;
};
const CopyInput = ({ label, value, className }: Props) => {
return (
<button
className={cn('w-full text-left', className)}
onClick={() => clipboard(value)}
>
{!!label && <Label>{label}</Label>}
<div className="flex items-center justify-between rounded border-input bg-def-200 p-2 px-3 font-mono text-sm">
{value}
<CopyIcon size={16} />
</div>
</button>
);
};
export default CopyInput;

View File

@@ -6,39 +6,56 @@ import type { InputProps } from '../ui/input';
import { Label } from '../ui/label';
import { Tooltiper } from '../ui/tooltip';
type InputWithLabelProps = InputProps & {
type WithLabel = {
children: React.ReactNode;
label: string;
error?: string | undefined;
info?: string;
className?: string;
};
type InputWithLabelProps = InputProps & Omit<WithLabel, 'children'>;
export const WithLabel = ({
children,
className,
label,
info,
error,
}: WithLabel) => {
return (
<div className={className}>
<div className="mb-2 flex items-end justify-between">
<Label
className="mb-0 flex flex-1 shrink-0 items-center gap-1 whitespace-nowrap"
htmlFor={label}
>
{label}
{info && (
<Tooltiper content={info}>
<InfoIcon size={14} />
</Tooltiper>
)}
</Label>
{error && (
<Tooltiper asChild content={error}>
<div className="flex items-center gap-1 text-sm leading-none text-destructive">
Issues
<BanIcon size={14} />
</div>
</Tooltiper>
)}
</div>
{children}
</div>
);
};
export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
({ label, className, info, ...props }, ref) => {
(props, ref) => {
return (
<div className={className}>
<div className="mb-2 flex items-end justify-between">
<Label
className="mb-0 flex flex-1 shrink-0 items-center gap-1 whitespace-nowrap"
htmlFor={label}
>
{label}
{info && (
<Tooltiper content={info}>
<InfoIcon size={14} />
</Tooltiper>
)}
</Label>
{props.error && (
<Tooltiper asChild content={props.error}>
<div className="flex items-center gap-1 text-sm leading-none text-destructive">
Issues
<BanIcon size={14} />
</div>
</Tooltiper>
)}
</div>
<Input ref={ref} id={label} {...props} />
</div>
<WithLabel {...props}>
<Input ref={ref} id={props.label} {...props} />
</WithLabel>
);
}
);

View File

@@ -0,0 +1,139 @@
// Based on Christin Alares tag input component (https://github.com/christianalares/seventy-seven)
import type { ElementRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import { useAnimate } from 'framer-motion';
import { XIcon } from 'lucide-react';
type Props = {
placeholder: string;
value: string[];
error?: string;
className?: string;
onChange: (value: string[]) => void;
renderTag?: (tag: string) => string;
};
const TagInput = ({
value: propValue,
onChange,
renderTag,
placeholder,
error,
}: Props) => {
const value = (
Array.isArray(propValue) ? propValue : propValue ? [propValue] : []
).filter(Boolean);
const [isMarkedForDeletion, setIsMarkedForDeletion] = useState(false);
const inputRef = useRef<ElementRef<'input'>>(null);
const [inputValue, setInputValue] = useState('');
const [scope, animate] = useAnimate();
const appendTag = (tag: string) => {
onChange([...value, tag]);
};
const removeTag = (tag: string) => {
onChange(value.filter((t) => t !== tag));
inputRef.current?.focus();
};
useEffect(() => {
if (inputValue.length > 0) {
setIsMarkedForDeletion(false);
}
}, [inputValue]);
return (
<div
ref={scope}
className={cn(
'inline-flex w-full flex-wrap items-center gap-2 rounded-md border border-input p-1 px-3 ring-offset-background has-[input:focus]:ring-2 has-[input:focus]:ring-ring has-[input:focus]:ring-offset-1',
!!error && 'border-destructive'
)}
>
{value.map((tag, i) => {
const isCreating = false;
return (
<span
data-tag={tag}
key={tag}
className={cn(
'inline-flex items-center gap-2 rounded bg-def-200 px-2 py-1 text-sm',
isMarkedForDeletion &&
i === value.length - 1 &&
'bg-destructive-foreground ring-2 ring-destructive/50 ring-offset-1',
isCreating && 'opacity-60'
)}
>
{renderTag ? renderTag(tag) : tag}
<Button
size="icon"
variant="outline"
className="h-4 w-4 rounded-full"
onClick={() => removeTag(tag)}
>
<span className="sr-only">Remove tag</span>
<XIcon name="close" className="size-3" />
</Button>
</span>
);
})}
<input
ref={inputRef}
placeholder={`${placeholder}`}
className="min-w-20 flex-1 py-1 text-sm focus-visible:outline-none"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const tagAlreadyExists = value.some(
(tag) => tag.toLowerCase() === inputValue.toLowerCase()
);
if (inputValue) {
if (tagAlreadyExists) {
animate(
`span[data-tag="${inputValue.toLowerCase()}"]`,
{
scale: [1, 1.3, 1],
},
{
duration: 0.3,
}
);
return;
}
appendTag(inputValue);
setInputValue('');
}
}
if (e.key === 'Backspace' && inputValue === '') {
if (!isMarkedForDeletion) {
setIsMarkedForDeletion(true);
return;
}
const last = value[value.length - 1];
if (last) {
removeTag(last);
}
setIsMarkedForDeletion(false);
setInputValue('');
}
}}
/>
</div>
);
};
export default TagInput;