feat: renamed old frontend and fixed some minor bugs

This commit is contained in:
=
2025-01-06 14:56:49 +05:30
parent 8c902d7699
commit 54377a44d3
2577 changed files with 35240 additions and 35116 deletions
@@ -0,0 +1,62 @@
import { faBugs, faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ErrorComponentProps, Link } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { Button } from "@app/components/v2";
export const ErrorPage = ({ error }: ErrorComponentProps) => {
return (
<div className="flex h-screen w-screen items-center justify-center bg-mineshaft-900">
<div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" />
<p>
Something went wrong. Please contact{" "}
<a
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
target="_blank"
rel="noopener noreferrer"
href="mailto:[email protected]"
>
support@infisical.com
</a>
, or{" "}
<a
href="https://infisical.com/slack"
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
target="_blank"
rel="noopener noreferrer"
>
join our Slack community
</a>{" "}
if the issue persists.
</p>
<Link to="/organization/secret-manager/overview">
<Button className="mt-4" size="xs">
<FontAwesomeIcon icon={faHome} className="mr-2" />
Back To Home
</Button>
</Link>
{error?.message && (
<>
<div className="my-4 h-px w-full bg-mineshaft-600" />
<p className="thin-scrollbar max-h-44 w-full overflow-auto text-ellipsis rounded-md bg-mineshaft-700 p-2">
<code className="text-xs">
{window.location.pathname}
<br />
{error.name}
<br />
{error.message}
</code>
<code className="font-mono text-xs">
{error instanceof AxiosError && (
<div className="mt-4">{JSON.stringify(error.response?.data)}</div>
)}
</code>
</p>
</>
)}
</div>
</div>
);
};
@@ -0,0 +1,33 @@
import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
export const NotFoundPage = () => {
return (
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen">
<Helmet>
<title>Infisical | Page Not Found</title>
</Helmet>
<div className="flex h-screen w-screen flex-col items-center justify-center text-gray-200">
<p className="mt-32 text-4xl">Oops, something went wrong</p>
<p className="mb-1 mt-2 text-lg">
Think this is a mistake? Email{" "}
<a className="text-primary underline underline-offset-4" href="mailto:[email protected]">
team@infisical.com
</a>{" "}
and we`ll fix it!{" "}
</p>
<Link to="/organization/secret-manager/overview">
<div className="diration-200 mt-8 cursor-default rounded-md bg-mineshaft-500 px-4 py-2 font-semibold hover:bg-primary hover:text-black">
Go to Dashboard
</div>
</Link>
<img
src="/images/dragon-404.svg"
height={554}
width={942}
alt="infisical dragon - page not found"
/>
</div>
</div>
);
};
@@ -0,0 +1,101 @@
import { Helmet } from "react-helmet";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ShareSecretForm } from "./components";
export const ShareSecretPage = () => {
return (
<>
<Helmet>
<title>Securely Share Secrets | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="" />
<meta name="og:description" content="" />
</Helmet>
<div className="dark h-full">
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div />
<div className="mx-auto w-full max-w-xl px-4 py-4 md:px-0">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<img
src="/images/gradientLogo.svg"
height={90}
width={120}
alt="Infisical logo"
className="cursor-pointer"
/>
</a>
</div>
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
Share a secret
</h1>
<p className="text-md">
Powered by{" "}
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical &rarr;
</a>
</p>
</div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<ShareSecretForm isPublic />
</div>
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
</p>
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs,
and certificates across your team and infrastructure.
</p>
<div className="mt-4 cursor-pointer sm:mt-0">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 px-3 py-2 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</a>
</div>
</div>
</div>
</div>
<div className="w-full bg-mineshaft-600 p-2">
<p className="text-center text-sm text-mineshaft-300">
Made with ❤️ by{" "}
<a className="text-primary" href="https://infisical.com">
Infisical
</a>
<br />
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
</p>
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1,264 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing";
// values in ms
const expiresInOptions = [
{ label: "5 min", value: 5 * 60 * 1000 },
{ label: "30 min", value: 30 * 60 * 1000 },
{ label: "1 hour", value: 60 * 60 * 1000 },
{ label: "1 day", value: 24 * 60 * 60 * 1000 },
{ label: "7 days", value: 7 * 24 * 60 * 60 * 1000 },
{ label: "14 days", value: 14 * 24 * 60 * 60 * 1000 },
{ label: "30 days", value: 30 * 24 * 60 * 60 * 1000 }
];
const viewLimitOptions = [
{ label: "1", value: 1 },
{ label: "Unlimited", value: -1 }
];
const schema = z.object({
name: z.string().optional(),
password: z.string().optional(),
secret: z.string().min(1),
expiresIn: z.string(),
viewLimit: z.string(),
accessType: z.nativeEnum(SecretSharingAccessType).optional()
});
export type FormData = z.infer<typeof schema>;
type Props = {
isPublic: boolean; // whether or not this is a public (non-authenticated) secret sharing form
value?: string;
};
export const ShareSecretForm = ({ isPublic, value }: Props) => {
const [secretLink, setSecretLink] = useState("");
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const publicSharedSecretCreator = useCreatePublicSharedSecret();
const privateSharedSecretCreator = useCreateSharedSecret();
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
const {
control,
reset,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
secret: value || ""
}
});
const onFormSubmit = async ({
name,
password,
secret,
expiresIn,
viewLimit,
accessType
}: FormData) => {
try {
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
const { id } = await createSharedSecret.mutateAsync({
name,
password,
secretValue: secret,
expiresAt,
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
accessType
});
const link = `${window.location.origin}/shared/secret/${id}`;
setSecretLink(link);
reset();
navigator.clipboard.writeText(link);
setCopyTextSecret("secret");
createNotification({
text: "Shared secret link copied to clipboard.",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to create a shared secret.",
type: "error"
});
}
};
const hasSecretLink = Boolean(secretLink);
return !hasSecretLink ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
{!isPublic && (
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name (Optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="API Key" type="text" />
</FormControl>
)}
/>
)}
<Controller
control={control}
name="secret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Your Secret"
isError={Boolean(error)}
errorText={error?.message}
className="mb-2"
isRequired
>
<textarea
placeholder="Enter sensitive data to share via an encrypted link..."
{...field}
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
disabled={value !== undefined}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="password"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password"
isError={Boolean(error)}
errorText={error?.message}
isOptional
>
<Input {...field} placeholder="Password" type="password" />
</FormControl>
)}
/>
<Controller
control={control}
name="expiresIn"
defaultValue="3600000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expiresInOptions.map(({ label, value: expiresInValue }) => (
<SelectItem value={String(expiresInValue || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="viewLimit"
defaultValue="-1"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Max Views" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{viewLimitOptions.map(({ label, value: viewLimitValue }) => (
<SelectItem value={String(viewLimitValue || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{!isPublic && (
<Controller
control={control}
name="accessType"
defaultValue={SecretSharingAccessType.Organization}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="General Access" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
<SelectItem value={SecretSharingAccessType.Organization}>
People within your organization
</SelectItem>
</Select>
</FormControl>
)}
/>
)}
<Button
className="mt-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create Secret Link
</Button>
</form>
) : (
<>
<div className="mr-2 flex items-center justify-end rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{secretLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(secretLink);
setCopyTextSecret("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
</IconButton>
</div>
<Button
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
colorSchema="primary"
variant="outline_bg"
size="sm"
onClick={() => setSecretLink("")}
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
>
Share Another Secret
</Button>
</>
);
};
@@ -0,0 +1 @@
export { ShareSecretForm } from "./ShareSecretForm";
@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShareSecretPage } from "./ShareSecretPage";
export const Route = createFileRoute("/share-secret")({
component: ShareSecretPage
});
@@ -0,0 +1,165 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useParams, useSearch } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { ROUTE_PATHS } from "@app/const/routes";
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
import { PasswordContainer, SecretContainer, SecretErrorContainer } from "./components";
const extractDetailsFromUrl = (urlEncodedKey: string) => {
if (urlEncodedKey) {
const [hashedHex, key] = urlEncodedKey ? urlEncodedKey.toString().split("-") : ["", ""];
return {
hashedHex,
key
};
}
return {
hashedHex: null,
key: null
};
};
export const ViewSharedSecretByIDPage = () => {
const id = useParams({
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
select: (el) => el.secretId
});
const urlEncodedKey = useSearch({
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
select: (el) => el.key
});
const [password, setPassword] = useState<string>();
const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey);
const {
data: fetchSecret,
error,
isPending,
isFetching
} = useGetActiveSharedSecretById({
sharedSecretId: id,
hashedHex,
password
});
const isInvalidCredential =
((error as AxiosError)?.response?.data as { message: string })?.message ===
"Invalid credentials";
const shouldShowPasswordPrompt =
isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret);
const isValidatingPassword = Boolean(password) && isFetching;
return (
<>
<Helmet>
<title>Securely Share Secrets | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="" />
<meta name="og:description" content="" />
</Helmet>
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div />
<div className="mx-auto w-full max-w-xl px-4 py-4 md:px-0">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<img
src="/images/gradientLogo.svg"
height={90}
width={120}
alt="Infisical logo"
className="cursor-pointer"
/>
</a>
</div>
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
View shared secret
</h1>
<p className="text-md">
Powered by{" "}
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical &rarr;
</a>
</p>
</div>
{(shouldShowPasswordPrompt || isValidatingPassword) && (
<PasswordContainer
isSubmitting={isValidatingPassword}
onPasswordSubmit={(el) => {
setPassword(el);
}}
isInvalidCredential={!isFetching && isInvalidCredential}
/>
)}
{!isPending && (
<>
{!error && fetchSecret?.secret && (
<SecretContainer secret={fetchSecret.secret} secretKey={key} />
)}
{error && !isInvalidCredential && <SecretErrorContainer />}
</>
)}
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
</p>
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs,
and certificates across your team and infrastructure.
</p>
<div className="mt-4 cursor-pointer sm:mt-0">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 px-3 py-2 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</a>
</div>
</div>
</div>
</div>
<div className="w-full bg-mineshaft-600 p-2">
<p className="text-center text-sm text-mineshaft-300">
Made with ❤️ by{" "}
<a className="text-primary" href="https://infisical.com">
Infisical
</a>
<br />
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
</p>
</div>
</div>
</>
);
};
@@ -0,0 +1,80 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
type Props = {
onPasswordSubmit: (val: any) => void;
isSubmitting?: boolean;
isInvalidCredential?: boolean;
};
const formSchema = z.object({
password: z.string()
});
export type FormData = z.infer<typeof formSchema>;
export const PasswordContainer = ({
onPasswordSubmit,
isSubmitting,
isInvalidCredential
}: Props) => {
const { control, handleSubmit } = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const onFormSubmit = async ({ password }: FormData) => {
onPasswordSubmit(password);
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="password"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error) || isInvalidCredential}
errorText={isInvalidCredential ? "Invalid credential" : error?.message}
isRequired
label="Password"
>
<div className="flex items-center justify-between gap-2 rounded-md">
<Input {...field} placeholder="Enter Password to view secret" type="password" />
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={handleSubmit(onFormSubmit)}
>
<FontAwesomeIcon
className={isSubmitting ? "fa-spin" : ""}
icon={isSubmitting ? faSpinner : faArrowRight}
/>
</IconButton>
</div>
</div>
</FormControl>
)}
/>
</form>
<Button
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
colorSchema="primary"
variant="outline_bg"
size="sm"
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
>
Share Your Own Secret
</Button>
</div>
);
};
@@ -0,0 +1,86 @@
import { useMemo } from "react";
import {
faArrowRight,
faCheck,
faCopy,
faEye,
faEyeSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import { Button, IconButton } from "@app/components/v2";
import { useTimedReset, useToggle } from "@app/hooks";
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
type Props = {
secret: TViewSharedSecretResponse["secret"];
secretKey: string | null;
};
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
const [isVisible, setIsVisible] = useToggle(false);
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const decryptedSecret = useMemo(() => {
if (secret.secretValue) {
return secret.secretValue;
}
if (secret && secret.encryptedValue && key) {
const res = decryptSymmetric({
ciphertext: secret.encryptedValue,
iv: secret.iv,
tag: secret.tag,
key
});
return res;
}
return "";
}, [secret, key]);
const hiddenSecret = decryptedSecret ? "*".repeat(decryptedSecret.length) : "";
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex items-center justify-between rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
<p className="whitespace-pre-wrap break-all">
{isVisible ? decryptedSecret : hiddenSecret}
</p>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(decryptedSecret);
setCopyTextSecret("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => setIsVisible.toggle()}
>
<FontAwesomeIcon icon={isVisible ? faEyeSlash : faEye} />
</IconButton>
</div>
</div>
<Button
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
colorSchema="primary"
variant="outline_bg"
size="sm"
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
>
Share Your Own Secret
</Button>
</div>
);
};
@@ -0,0 +1,13 @@
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const SecretErrorContainer = () => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center">
<FontAwesomeIcon icon={faKey} size="2x" />
<p className="mt-4">The secret you are looking is missing or has expired</p>
</div>
</div>
);
};
@@ -0,0 +1,3 @@
export { PasswordContainer } from "./PasswordContainer";
export { SecretContainer } from "./SecretContainer";
export { SecretErrorContainer } from "./SecretErrorContainer";
@@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage";
const SharedSecretByIDPageQuerySchema = z.object({
key: z.string()
});
export const Route = createFileRoute("/shared/secret/$secretId")({
component: ViewSharedSecretByIDPage,
validateSearch: zodValidator(SharedSecretByIDPageQuerySchema)
});