Skip to content

Commit

Permalink
Merge pull request #5 from rocky-linux/feature/news
Browse files Browse the repository at this point in the history
Finish News System Implementation
  • Loading branch information
NebraskaCoder authored Feb 19, 2024
2 parents 64fe3bb + 9a86b6e commit 1edab53
Show file tree
Hide file tree
Showing 15 changed files with 858 additions and 7 deletions.
2 changes: 1 addition & 1 deletion app/[locale]/news/[slug]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function SlugNotFound() {
const t = useTranslations("news.notFound");

return (
<div className="pt-10 pb-24 sm:pt-12 sm:pb-32">
<div className="py-24 sm:py-32">
<div className="mx-auto max-w-3xl text-base leading-7">
<h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl mb-12 text-center font-display">
{t("title")}
Expand Down
6 changes: 4 additions & 2 deletions app/[locale]/news/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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`,
};
}

Expand All @@ -52,9 +53,10 @@ export default async function Post({ params }: Props) {
{postData.title}
</h1>
<div
className="prose dark:prose-invert prose-headings:font-display prose-a:text-primary prose-pre:bg-muted prose-pre:py-3 prose-pre:px-4 prose-pre:rounded prose-img:rounded-md max-w-none"
className="prose dark:prose-invert prose-headings:font-display prose-a:text-primary prose-pre:bg-muted prose-pre:py-3 prose-pre:px-4 prose-pre:rounded prose-img:rounded-md max-w-none mb-12"
dangerouslySetInnerHTML={{ __html: postData.contentHtml }}
/>
<ShareButtons url={`https://rockylinux.org/news/${params.slug}`} />
</div>
</div>
);
Expand Down
70 changes: 70 additions & 0 deletions app/[locale]/news/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="py-24 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl font-bold font-display tracking-tight sm:text-4xl">
{t("title")}
</h2>
<p className="mt-2 text-lg leading-8">{t("description")}</p>
</div>
<div className="mx-auto mt-10 grid max-w-2xl grid-cols-1 gap-8 border-t pt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none lg:grid-cols-3">
{posts.map((post) => (
<Link
key={post.slug}
href={`/news/${post.slug}`}
>
<Card key={post.slug}>
<CardHeader>
<CardTitle className="font-display font-bold truncate">
{post.title}
</CardTitle>
<CardDescription>
{format(new Date(post.date), "MMMM d, yyyy")}
</CardDescription>
</CardHeader>
<CardContent>
<div className="group relative">
<p className="line-clamp-3 text-sm leading-6">
{post.excerpt}
</p>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
</div>
);
};

export default NewsPage;
104 changes: 104 additions & 0 deletions components/shareButtons/MastodonDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<MastodonDialogProps> = ({
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<z.infer<typeof mastodonShareSchema>>({
resolver: zodResolver(mastodonShareSchema),
defaultValues: {
instanceUrl: "",
},
});

function onSubmit(values: z.infer<typeof mastodonShareSchema>) {
const shareUrl = `https://${values.instanceUrl}/share?text=${url}`;

window.open(shareUrl, "_blank");
}
return (
<>
<Popover>
<PopoverTrigger>
<span className="sr-only">{mastodonSrMsg}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className="h-5"
viewBox="0 0 16 16"
>
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z" />
</svg>
</PopoverTrigger>
<PopoverContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="instanceUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{urlMsg}</FormLabel>
<FormControl>
<Input
placeholder="mastodon.social"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="mt-4"
>
{shareMsg}
</Button>
</form>
</Form>
</PopoverContent>
</Popover>
</>
);
};

export default MastodonDialog;
82 changes: 82 additions & 0 deletions components/shareButtons/ShareButtons.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h3 className="text-sm text-center mb-2">{t("shareName")}</h3>
<div className="flex space-x-6 justify-center mb-12">
<MastodonDialog
invalidUrlMsg={t("mastodon.url-valid")}
urlMsg={t("mastodon.url")}
shareMsg={t("shareName")}
mastodonSrMsg={t("mastodon.name")}
url={url}
/>
<Link
href={facebookLink}
className="hover:text-primary"
target="_blank"
>
<span className="sr-only">{t("facebook")}</span>
<svg
fill="currentColor"
viewBox="0 0 24 24"
className="h-6 w-6"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
clipRule="evenodd"
/>
</svg>
</Link>
<Link
href={linkedInLink}
className="hover:text-primary"
target="_blank"
>
<span className="sr-only">{t("linkedin")}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className="h-6 w-6"
aria-hidden="true"
>
<path d="M0 0v24h24v-24h-24zm8 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.397-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</Link>
<Link
href={xLink}
className="hover:text-primary"
target="_blank"
>
<span className="sr-only">{t("x")}</span>
<svg
fill="currentColor"
viewBox="0 0 24 24"
className="h-6 w-6"
aria-hidden="true"
>
<path d="M13.6823 10.6218L20.2391 3H18.6854L12.9921 9.61788L8.44486 3H3.2002L10.0765 13.0074L3.2002 21H4.75404L10.7663 14.0113L15.5685 21H20.8131L13.6819 10.6218H13.6823ZM11.5541 13.0956L10.8574 12.0991L5.31391 4.16971H7.70053L12.1742 10.5689L12.8709 11.5655L18.6861 19.8835H16.2995L11.5541 13.096V13.0956Z" />
</svg>
</Link>
</div>
</>
);
};

export default ShareButtons;
87 changes: 87 additions & 0 deletions components/ui/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from "react";

import { cn } from "@/lib/utils";

const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
));
Card.displayName = "Card";

const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";

const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";

const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";

const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("p-6 pt-0", className)}
{...props}
/>
));
CardContent.displayName = "CardContent";

const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";

export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
Loading

0 comments on commit 1edab53

Please sign in to comment.