Skip to content

Commit

Permalink
Merge pull request #88 from dotslashf/development
Browse files Browse the repository at this point in the history
Custom Tweet Component
  • Loading branch information
dotslashf authored Oct 3, 2024
2 parents 2e0695e + 71461f7 commit 7e60606
Show file tree
Hide file tree
Showing 20 changed files with 528 additions and 48 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@next/third-parties": "^14.2.5",
"@prisma/client": "^5.14.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
Expand Down
55 changes: 39 additions & 16 deletions src/app/_components/CreateCopyPastaPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { createCopyPastaFormClient } from "../../server/form/copyPasta";
import { type z } from "zod";
import useToast from "~/components/ui/use-react-hot-toast";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import MultipleSelector, {
type Option,
} from "~/components/ui/multiple-selector";
Expand All @@ -45,11 +45,9 @@ import { id } from "date-fns/locale";
import { DAYS, parseErrorMessages } from "~/lib/constant";
import BreadCrumbs from "~/components/BreadCrumbs";
import Link from "next/link";
import { Tweet } from "react-tweet";
import { type Tweet as TweetInterface } from "react-tweet/api";
import { parseISO } from "date-fns";
import { useToBlob } from "@hugocxl/react-to-image";
import he from "he";
import TweetPage from "./TweetPage";
import EmptyState from "~/components/EmptyState";

export default function CreateCopyPasta() {
const [tags] = api.tag.list.useSuspenseQuery(undefined, {
Expand Down Expand Up @@ -102,6 +100,19 @@ export default function CreateCopyPasta() {
}
};

const shouldRunEffect = useMemo(() => modeCreate === "auto", [modeCreate]);

useEffect(() => {
if (shouldRunEffect) {
if (Object.values(form.formState.errors).length > 0) {
form.setError("sourceUrl", {
message: "Isi tweet terlalu pendek untuk dijadikan template",
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.formState.errors.content, shouldRunEffect]);

async function onSubmit(values: z.infer<typeof createCopyPastaFormClient>) {
try {
if (
Expand Down Expand Up @@ -203,17 +214,17 @@ export default function CreateCopyPasta() {
const tweetId = getTweetId(form.getValues("sourceUrl") ?? "");
if (!tweetId) return null;
setFetchedTweetId(tweetId);
const url = `https://react-tweet.vercel.app/api/tweet/${tweetId}`;

try {
const response = await fetch(url, {});
const { data } = (await response.json()) as { data: TweetInterface };
}

form.setValue("content", he.decode(data.text));
form.setValue("postedAt", parseISO(data.created_at));
} catch (error) {
console.error("Error fetching tweet data:", error);
}
function handleTweetDataFetched({
content,
postedAt,
}: {
content: string;
postedAt: Date;
}) {
form.setValue("postedAt", postedAt);
form.setValue("content", content);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -422,14 +433,26 @@ export default function CreateCopyPasta() {
/>
</div>
)}
{modeCreate === "auto" && (
<span className="text-sm">Tweet akan muncul dibawah 👇</span>
)}
{fetchedTweetId && (
<div
ref={ref}
className="flex items-center justify-center bg-background"
>
<Tweet id={fetchedTweetId} />
<TweetPage
id={fetchedTweetId}
onTweetLoaded={handleTweetDataFetched}
/>
</div>
)}
{modeCreate === "auto" && !fetchedTweetId && (
<EmptyState
message="Tweet belum terload"
className="border-solid"
/>
)}
</div>
<div className="mt-6 w-full">
<Button
Expand Down
2 changes: 1 addition & 1 deletion src/app/_components/RankingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
AccordionTrigger,
} from "~/components/ui/accordion";
import Link from "next/link";
import Avatar from "~/components/ui/avatar";
import Avatar from "~/components/ui/avatar-image";
import BreadCrumbs from "~/components/BreadCrumbs";
import { usePathname } from "next/navigation";
import { Laugh, Library, NotebookPen } from "lucide-react";
Expand Down
68 changes: 68 additions & 0 deletions src/app/_components/TweetPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { parseISO } from "date-fns";
import { X } from "lucide-react";
import React, { useState, useEffect } from "react";
import { enrichTweet } from "react-tweet";
import { type Tweet } from "react-tweet/api";
import EmptyState from "~/components/EmptyState";
import CustomTweet from "~/components/Tweet/CustomTweet";
import SkeletonTweet from "~/components/Tweet/SkeletonTweet";
import { sanitizeTweetEnrich } from "~/lib/utils";

interface TweetPageProps {
id: string;
onTweetLoaded: ({
content,
postedAt,
}: {
content: string;
postedAt: Date;
}) => void;
}
export default function TweetPage({ id, onTweetLoaded }: TweetPageProps) {
const [tweet, setTweet] = useState<Tweet | undefined | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
const fetchTweet = async () => {
setError(null);
try {
setIsLoading(true);
const response = await fetch(`/api/tweet/${id}`);
if (!response.ok) {
throw new Error("Failed to fetch tweet");
}
const { tweet }: { tweet: Tweet } = await response.json();
setTweet(tweet);
const enrich = enrichTweet(tweet);
onTweetLoaded({
content: sanitizeTweetEnrich(enrich),
postedAt: parseISO(tweet.created_at),
});
} catch (err) {
console.error(err);
setError(err instanceof Error ? err : new Error("An error occurred"));
} finally {
setIsLoading(false);
}
};

void fetchTweet();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);

if (error)
return (
<EmptyState
className="border-solid text-lg"
message={
<span className="inline-flex items-center">
Tweet tidak ditemukan <X className="ml-2 inline" />
</span>
}
/>
);
if (!tweet || isLoading) return <SkeletonTweet />;

return <CustomTweet tweet={tweet} />;
}
2 changes: 1 addition & 1 deletion src/app/api/email/preview/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function GET(request: Request) {
break;
}

const html = render(templateComponent);
const html = await render(templateComponent);
const response = new NextResponse(html);
response.headers.set("Content-Type", "text/html; charset=utf-8");
return response;
Expand Down
43 changes: 43 additions & 0 deletions src/app/api/tweet/[id]/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { unstable_cache } from "next/cache";
import { NextResponse, type NextRequest } from "next/server";
import { getTweet as _getTweet } from "react-tweet/api";

const getTweet = unstable_cache(
async (id: string) => _getTweet(id),
["tweet"],
{ revalidate: 3600 * 24 },
);

export async function GET(
_: NextRequest,
{ params }: { params: { id: string } },
) {
try {
const tweet = await getTweet(params.id);
if (!tweet) {
return NextResponse.json(
{ error: "Failed to fetch tweet" },
{
status: 404,
headers: {
"content-type": "application/json",
},
},
);
}
return NextResponse.json({
tweet,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Failed to fetch tweet" },
{
status: 404,
headers: {
"content-type": "application/json",
},
},
);
}
}
2 changes: 1 addition & 1 deletion src/components/Collection/CardCollectionDescription.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CalendarDays, Check, ChevronRight, Pencil, Trash } from "lucide-react";
import { formatDateToHuman, cn } from "~/lib/utils";
import Avatar from "../ui/avatar";
import Avatar from "../ui/avatar-image";
import { Badge, badgeVariants } from "../ui/badge";
import {
Card,
Expand Down
2 changes: 1 addition & 1 deletion src/components/CopyPasta/CardById.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { type CardProps } from "~/lib/interface";
import { Badge, badgeVariants } from "../ui/badge";
import { api } from "~/trpc/react";
import { trackEvent } from "~/lib/track";
import Avatar from "../ui/avatar";
import Avatar from "../ui/avatar-image";
import { useState } from "react";
import DialogImage from "./DialogImage";

Expand Down
2 changes: 1 addition & 1 deletion src/components/CopyPasta/CardMinimal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { type Tag as TagType } from "@prisma/client";
import Tag from "../ui/tags";
import useToast from "../ui/use-react-hot-toast";
import { trackEvent } from "~/lib/track";
import Avatar from "../ui/avatar";
import Avatar from "../ui/avatar-image";
import { badgeVariants } from "../ui/badge";
import { useState } from "react";
import DialogImage from "./DialogImage";
Expand Down
4 changes: 2 additions & 2 deletions src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type ReactElement, type HTMLAttributes } from "react";
import { cn } from "~/lib/utils";

interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
message?: string;
message?: string | ReactElement;
action?: ReactElement;
}
export default function EmptyState({
Expand All @@ -14,7 +14,7 @@ export default function EmptyState({
<div
className={cn(
"flex h-32 w-full flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed bg-secondary text-sm",
{ ...props },
props.className,
)}
>
{message}
Expand Down
2 changes: 1 addition & 1 deletion src/components/NavbarDropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { useMediaQuery } from "@uidotdev/usehooks";
import { trackEvent } from "~/lib/track";
import { ANALYTICS_EVENT } from "~/lib/constant";
import { type Session } from "next-auth";
import Avatar from "./ui/avatar";
import Avatar from "./ui/avatar-image";
import { signOut } from "next-auth/react";

interface NavbarDropDownProps {
Expand Down
Loading

0 comments on commit 7e60606

Please sign in to comment.