Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/v3/generic/Field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit items-center gap-1.5 text-xs leading-snug text-accent group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 [&>*]:data-[slot=field]:p-2.5",
"group/field-label peer/field-label flex w-fit items-center gap-1.5 text-xs leading-snug text-accent group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 [&>*]:data-[slot=field]:p-2.5 [&>svg]:size-3",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/v3/generic/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { forwardRef } from "react";

import { cn } from "../../utils";

function UnstableInput({ className, type, ...props }: React.ComponentProps<"input">) {
const UnstableInput = forwardRef<
HTMLInputElement,
React.ComponentProps<"input"> & { isError?: boolean }
>(({ className, type, isError, ...props }, ref) => {
return (
<input
ref={ref}
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-border bg-transparent px-3 py-1 text-sm 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 file:text-foreground placeholder:text-muted disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40",
"selection:bg-foreground selection:text-background",
className
)}
aria-invalid={isError}
{...props}
/>
);
}
});

UnstableInput.displayName = "UnstableInput";

export { UnstableInput };
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { useMemo, useState } from "react";
import { CheckIcon, CopyIcon, KeyRoundIcon, RefreshCwIcon } from "lucide-react";

import { useTimedReset } from "@app/hooks";

import { Button } from "../Button";
import { Checkbox } from "../Checkbox";
import { UnstableIconButton } from "../IconButton";
import { Label } from "../Label";
import { Popover, PopoverContent, PopoverTrigger } from "../Popover";

type PasswordOptionsType = {
length: number;
useUppercase: boolean;
useLowercase: boolean;
useNumbers: boolean;
useSpecialChars: boolean;
};

export type PasswordGeneratorProps = {
onUsePassword?: (password: string) => void;
isDisabled?: boolean;
minLength?: number;
maxLength?: number;
};

export const PasswordGenerator = ({
onUsePassword,
isDisabled = false,
minLength = 12,
maxLength = 64
}: PasswordGeneratorProps) => {
const [isOpen, setIsOpen] = useState(false);
const [, isCopying, setCopyText] = useTimedReset<string>({
initialState: "Copy"
});
const [refresh, setRefresh] = useState(false);
const [passwordOptions, setPasswordOptions] = useState<PasswordOptionsType>({
length: minLength,
useUppercase: true,
useLowercase: true,
useNumbers: true,
useSpecialChars: true
});

const password = useMemo(() => {
const charset = {
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
lowercase: "abcdefghijklmnopqrstuvwxyz",
numbers: "0123456789",
specialChars: "-_.~!*"
};

let availableChars = "";
if (passwordOptions.useUppercase) availableChars += charset.uppercase;
if (passwordOptions.useLowercase) availableChars += charset.lowercase;
if (passwordOptions.useNumbers) availableChars += charset.numbers;
if (passwordOptions.useSpecialChars) availableChars += charset.specialChars;

if (availableChars === "") availableChars = charset.lowercase + charset.numbers;

let newPassword = "";
for (let i = 0; i < passwordOptions.length; i += 1) {
const randomIndex = Math.floor(Math.random() * availableChars.length);
newPassword += availableChars[randomIndex];
}

return newPassword;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [passwordOptions, refresh]);

const copyToClipboard = () => {
navigator.clipboard
.writeText(password)
.then(() => {
setCopyText("Copied");
})
.catch(() => {
setCopyText("Copy failed");
});
};

const usePassword = () => {
onUsePassword?.(password);
setIsOpen(false);
};

return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<UnstableIconButton variant="outline" size="md" isDisabled={isDisabled}>
<KeyRoundIcon />
</UnstableIconButton>
</PopoverTrigger>
<PopoverContent className="w-[30rem]" align="end">
<div className="flex flex-col gap-4">
<div>
<p className="text-sm font-medium">Generate Random Value</p>
<p className="mt-0.5 text-xs text-muted">Generate strong unique values</p>
</div>

<div className="rounded-md border border-border bg-container/50 p-3">
<div className="flex items-center justify-between gap-2">
<p className="flex-1 font-mono text-sm break-all select-all">{password}</p>
<div className="flex shrink-0 gap-1">
<UnstableIconButton
variant="ghost"
size="xs"
onClick={() => setRefresh((prev) => !prev)}
>
<RefreshCwIcon />
</UnstableIconButton>
<UnstableIconButton variant="ghost" size="xs" onClick={copyToClipboard}>
{isCopying ? <CheckIcon /> : <CopyIcon />}
</UnstableIconButton>
</div>
</div>
</div>

<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-xs text-accent">Length: {passwordOptions.length}</Label>
</div>
<input
type="range"
min={minLength}
max={maxLength}
value={passwordOptions.length}
onChange={(e) =>
setPasswordOptions({ ...passwordOptions, length: Number(e.target.value) })
}
className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-foreground/10 accent-project [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow-sm"
/>
</div>

<div className="flex flex-wrap gap-6">
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useUppercase}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useUppercase: checked as boolean })
}
/>
A-Z
</Label>
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useLowercase}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useLowercase: checked as boolean })
}
/>
a-z
</Label>
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useNumbers}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useNumbers: checked as boolean })
}
/>
0-9
</Label>
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useSpecialChars}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useSpecialChars: checked as boolean })
}
/>
-_.~!*
</Label>
</div>

{onUsePassword && (
<Button variant="project" size="xs" onClick={usePassword} className="w-full">
Use Value
</Button>
)}
</div>
</PopoverContent>
</Popover>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PasswordGenerator, type PasswordGeneratorProps } from "./PasswordGenerator";
Loading
Loading