diff --git a/app/[locale]/news/[slug]/not-found.tsx b/app/[locale]/news/[slug]/not-found.tsx
index 92d0cc14..ee83448a 100644
--- a/app/[locale]/news/[slug]/not-found.tsx
+++ b/app/[locale]/news/[slug]/not-found.tsx
@@ -5,7 +5,7 @@ export default function SlugNotFound() {
const t = useTranslations("news.notFound");
return (
-
+
{t("title")}
diff --git a/app/[locale]/news/[slug]/page.tsx b/app/[locale]/news/[slug]/page.tsx
index c2370d9d..10b196a9 100644
--- a/app/[locale]/news/[slug]/page.tsx
+++ b/app/[locale]/news/[slug]/page.tsx
@@ -1,4 +1,5 @@
import Date from "@/components/Date";
+import ShareButtons from "@/components/shareButtons/ShareButtons";
import { checkIfSlugIsValid, getPostData } from "@/lib/news";
import { notFound } from "next/navigation";
@@ -29,7 +30,7 @@ export async function generateMetadata({ params }: Props) {
const postData: PostData = await getPostData(slug);
return {
- title: postData.title,
+ title: `${postData.title} - Rocky Linux`,
};
}
@@ -52,9 +53,10 @@ export default async function Post({ params }: Props) {
{postData.title}
+
);
diff --git a/app/[locale]/news/page.tsx b/app/[locale]/news/page.tsx
new file mode 100644
index 00000000..0847476a
--- /dev/null
+++ b/app/[locale]/news/page.tsx
@@ -0,0 +1,70 @@
+import type { NextPage } from "next";
+
+import { getTranslations } from "next-intl/server";
+import Link from "next/link";
+import { format } from "date-fns";
+
+import { getSortedPostsData } from "@/lib/news";
+
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+export async function generateMetadata() {
+ const t = await getTranslations("news");
+
+ return {
+ title: `${t("title")} - Rocky Linux`,
+ description: `${t("description")}`,
+ };
+}
+
+const NewsPage: NextPage = async () => {
+ const posts = await getSortedPostsData();
+ const t = await getTranslations("news");
+
+ return (
+
+
+
+
+ {t("title")}
+
+
{t("description")}
+
+
+ {posts.map((post) => (
+
+
+
+
+ {post.title}
+
+
+ {format(new Date(post.date), "MMMM d, yyyy")}
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default NewsPage;
diff --git a/components/shareButtons/MastodonDialog.tsx b/components/shareButtons/MastodonDialog.tsx
new file mode 100644
index 00000000..9bfc580e
--- /dev/null
+++ b/components/shareButtons/MastodonDialog.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import * as React from "react";
+import * as z from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Input } from "@/components/ui/input";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Button } from "@/components/ui/button";
+
+export interface MastodonDialogProps {
+ invalidUrlMsg: string;
+ urlMsg: string;
+ shareMsg: string;
+ mastodonSrMsg: string;
+ url: string;
+}
+
+const MastodonDialog: React.FC
= ({
+ invalidUrlMsg,
+ urlMsg,
+ shareMsg,
+ mastodonSrMsg,
+ url,
+}) => {
+ const mastodonShareSchema = z.object({
+ instanceUrl: z
+ .string()
+ .regex(/^(?:[a-zA-Z0-9\-_]+\.)+[a-zA-Z]{2,}$/, invalidUrlMsg),
+ });
+
+ const form = useForm>({
+ resolver: zodResolver(mastodonShareSchema),
+ defaultValues: {
+ instanceUrl: "",
+ },
+ });
+
+ function onSubmit(values: z.infer) {
+ const shareUrl = `https://${values.instanceUrl}/share?text=${url}`;
+
+ window.open(shareUrl, "_blank");
+ }
+ return (
+ <>
+
+
+ {mastodonSrMsg}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default MastodonDialog;
diff --git a/components/shareButtons/ShareButtons.tsx b/components/shareButtons/ShareButtons.tsx
new file mode 100644
index 00000000..4c9759c1
--- /dev/null
+++ b/components/shareButtons/ShareButtons.tsx
@@ -0,0 +1,82 @@
+import { useTranslations } from "next-intl";
+import Link from "next/link";
+
+import MastodonDialog from "./MastodonDialog";
+
+interface ShareButtonsProps {
+ url: string;
+}
+
+const ShareButtons = ({ url }: ShareButtonsProps) => {
+ const t = useTranslations("share");
+
+ const facebookLink = `https://www.facebook.com/sharer/sharer.php?u=${url}`;
+ const xLink = `https://twitter.com/intent/tweet?text=${url}`;
+ const linkedInLink = `https://www.linkedin.com/sharing/share-offsite/?url=${url}`;
+
+ return (
+ <>
+ {t("shareName")}
+
+
+
+
{t("facebook")}
+
+
+
+
{t("linkedin")}
+
+
+
+
{t("x")}
+
+
+
+ >
+ );
+};
+
+export default ShareButtons;
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 00000000..07f51513
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,87 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/components/ui/form.tsx b/components/ui/form.tsx
new file mode 100644
index 00000000..b1a5c1ed
--- /dev/null
+++ b/components/ui/form.tsx
@@ -0,0 +1,181 @@
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form";
+
+import { cn } from "@/lib/utils";
+import { Label } from "@/components/ui/label";
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ");
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+);
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+});
+FormItem.displayName = "FormItem";
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = "FormLabel";
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = "FormControl";
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = "FormDescription";
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = "FormMessage";
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+};
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 00000000..f6b24530
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 00000000..40378d43
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
new file mode 100644
index 00000000..b7a19e9d
--- /dev/null
+++ b/components/ui/popover.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "@/lib/utils";
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverAnchor = PopoverPrimitive.Anchor;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/lib/__tests__/news.test.ts b/lib/__tests__/news.test.ts
index f8212a6f..b329c7e9 100644
--- a/lib/__tests__/news.test.ts
+++ b/lib/__tests__/news.test.ts
@@ -74,8 +74,18 @@ describe("News Library", () => {
it("should return an array of sorted post data", async () => {
const sortedPosts = await newsLib.getSortedPostsData();
expect(sortedPosts).toStrictEqual([
- { slug: "post2", date: "2022-01-02", title: "Post 2" },
- { slug: "post1", date: "2022-01-01", title: "Post 1" },
+ {
+ slug: "post2",
+ excerpt: "Mocked HTML content...",
+ date: "2022-01-02",
+ title: "Post 2",
+ },
+ {
+ slug: "post1",
+ excerpt: "Mocked HTML content...",
+ date: "2022-01-01",
+ title: "Post 1",
+ },
]);
});
});
diff --git a/lib/news.ts b/lib/news.ts
index 36b23775..33d42574 100644
--- a/lib/news.ts
+++ b/lib/news.ts
@@ -38,8 +38,14 @@ export async function getSortedPostsData() {
const matterResult = matter(fileContents);
+ const contentHtml = await processMarkdownAsHTML(matterResult.content);
+
+ const plainText = contentHtml.replace(/<[^>]*>/g, "");
+ const excerpt = plainText.substring(0, 200) + "...";
+
return {
slug,
+ excerpt,
...(matterResult.data as { date: string; title: string }),
};
})
@@ -79,9 +85,13 @@ export async function getPostData(slug: string) {
const contentHtml = await processMarkdownAsHTML(matterResult.content);
+ const plainText = contentHtml.replace(/<[^>]*>/g, "");
+ const excerpt = plainText.substring(0, 200) + "...";
+
return {
slug,
contentHtml,
+ excerpt,
...(matterResult.data as { date: string; title: string }),
};
}
diff --git a/messages/en.json b/messages/en.json
index 257932c8..d6a4db98 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -68,9 +68,22 @@
}
},
"news": {
+ "title": "News",
+ "description": "Stay up to date with the latest news and updates from the Rocky Linux community.",
"notFound": {
"title": "Not Found",
"description": "The news post you are looking for does not exist. You can find all our news posts on the news page."
}
+ },
+ "share": {
+ "shareName": "Share",
+ "mastodon": {
+ "name": "Mastodon",
+ "url-valid": "Please enter a valid Mastodon instance URL (without https://).",
+ "url": "Mastodon Instance URL"
+ },
+ "facebook": "Facebook",
+ "linkedin": "LinkedIn",
+ "x": "X"
}
}
diff --git a/package-lock.json b/package-lock.json
index e1ea0f3b..8c4f43f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,11 +10,14 @@
"dependencies": {
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
+ "@hookform/resolvers": "^3.3.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
+ "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-slot": "^1.0.2",
"@types/node": "20.11.16",
"@types/react": "18.2.55",
@@ -34,6 +37,7 @@
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.0",
@@ -44,7 +48,8 @@
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.3.3",
- "unified": "^11.0.4"
+ "unified": "^11.0.4",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@storybook/addon-a11y": "^7.6.13",
@@ -3005,6 +3010,14 @@
"react": ">= 16"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
+ "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -4456,6 +4469,29 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz",
+ "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
@@ -4666,6 +4702,150 @@
}
}
},
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
+ "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-focus-guards": "1.0.1",
+ "@radix-ui/react-focus-scope": "1.0.4",
+ "@radix-ui/react-id": "1.0.1",
+ "@radix-ui/react-popper": "1.1.3",
+ "@radix-ui/react-portal": "1.0.4",
+ "@radix-ui/react-presence": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-slot": "1.0.2",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.5.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
+ "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-escape-keydown": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
+ "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
+ "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.0.3",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1",
+ "@radix-ui/react-use-rect": "1.0.1",
+ "@radix-ui/react-use-size": "1.0.1",
+ "@radix-ui/rect": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
+ "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz",
@@ -23900,6 +24080,21 @@
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
"dev": true
},
+ "node_modules/react-hook-form": {
+ "version": "7.50.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz",
+ "integrity": "sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-icons": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz",
@@ -28016,6 +28211,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/package.json b/package.json
index f0cb4259..72c7adf2 100644
--- a/package.json
+++ b/package.json
@@ -20,11 +20,14 @@
"dependencies": {
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
+ "@hookform/resolvers": "^3.3.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
+ "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-slot": "^1.0.2",
"@types/node": "20.11.16",
"@types/react": "18.2.55",
@@ -44,6 +47,7 @@
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.0",
@@ -54,7 +58,8 @@
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.3.3",
- "unified": "^11.0.4"
+ "unified": "^11.0.4",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@storybook/addon-a11y": "^7.6.13",