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")} + + + +
+

+ {post.excerpt} +

+
+
+
+ + ))} +
+
+
+ ); +}; + +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} + + + + + +
+ + ( + + {urlMsg} + + + + + + )} + /> + + + +
+
+ + ); +}; + +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 ( +